"""
GSUN View

作者: Guoyou Sun (孙国佑)
网站: http://sunguoyou.lamost.org/
版本: 2.0

版权所有 © 2025-2030 Guoyou Sun (孙国佑)

免责声明:
本软件为原创作品，仅用于天文科学研究和教育目的。
未经作者明确许可，禁止商业使用、复制、分发或修改本软件。
作者对使用本软件导致的任何直接或间接损失不承担责任。

All rights reserved. This software is the confidential and proprietary information
of Guoyou Sun. You shall not disclose such confidential information and shall
use it only in accordance with the terms of the license agreement.
"""
# -*- coding: utf-8 -*-
import sys
from BatchAlignThread import BatchAlignThread
from BatchFlatfieldThread import BatchFlatfieldThread
from SingleFlatfieldThread import SingleFlatfieldThread
import os
import json
import csv
import re
import logging
import threading
import time
from pathlib import Path
from functools import partial
from PyQt5.QtWidgets import (QRadioButton,QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QPushButton, QFileDialog, QListWidget, QSplitter,
                             QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QMenu,
                             QAction, QTreeWidget, QTreeWidgetItem, QTextEdit, QSlider,
                             QCheckBox, QMessageBox, QFrame, QListWidgetItem, QGroupBox,
                             QLineEdit, QToolBar, QToolButton, QDialog, QTabWidget, QStyle, QSpinBox, QDoubleSpinBox, QComboBox,
                             QSizePolicy, QProgressDialog, QAbstractItemView, QDialogButtonBox, QHeaderView,
                             QFormLayout, QProgressBar, QTableWidget, QTableWidgetItem, QGraphicsLineItem)
from PyQt5.QtWebEngineWidgets import QWebEngineView
from app import FitsImageProcessor
from PyQt5.QtGui import QPixmap, QImage, QTransform, QCursor, QColor, QPainter, QPen, QBrush, QIcon, QFont, QLinearGradient
from PyQt5.QtCore import Qt, QRectF, QPointF, QTimer, pyqtSignal, QSize, QSettings, QUrl, QRect, QLineF
import numpy as np
from PIL import Image
from astropy.io import fits
from datetime import datetime
import warnings
from astropy.io.fits.verify import VerifyWarning
warnings.filterwarnings('ignore', category=VerifyWarning)
from history_logger import HistoryLogger
import glob
import re
import pandas as pd
import urllib.parse
import urllib.request
import gzip
import requests
from bs4 import BeautifulSoup
import configparser
import re
from catalog_collection_dialog import CatalogCollectionDialog
from PyQt5.QtCore import QThread
from PyQt5.QtGui import QDesktopServices
import concurrent.futures
import asteroid_worker
from astropy.coordinates import SkyCoord, solar_system_ephemeris, get_body_barycentric
from astropy import units as u
from astropy.time import Time
import astroalign as aa


class CatalogCollectionThread(QThread):
    """目录采集线程"""
    progress_signal = pyqtSignal(str)  # 进度信号
    completed_signal = pyqtSignal(pd.DataFrame)  # 完成信号，返回采集的数据表
    error_signal = pyqtSignal(str)  # 错误信号
    
    def __init__(self, base_url="https://download.china-vo.org/", selected_months=None):
        super().__init__()
        self.base_url = base_url
        self.is_cancelled = False
        self.catalog_data = []  # 存储采集的数据
        self.selected_months = selected_months or {}  # 选中的月份 {year: [month1, month2, ...]}
        
    def cancel(self):
        """取消采集操作"""
        self.is_cancelled = True
        self.progress_signal.emit("用户取消了采集操作")
        
    def run(self):
        try:
            self.progress_signal.emit("开始采集目录数据...")
            
            # 创建数据表的列
            columns = ["年份", "月份", "日期", "文件夹", "文件名", "文件大小", "URL", "更新时间"]
            
            # 如果没有选择任何月份，则使用默认的2020-2030年
            if not self.selected_months:
                # 使用2020-2030年的年份范围
                years = list(range(2020, 2031))
                
                # 按照降序排列年份（最近的年份在前）
                years.sort(reverse=True)
                
                # 搜索这些年份的数据
                for year in years:
                    if self.is_cancelled:
                        break
                    self._collect_year_data(str(year))
            else:
                # 按照选择的年份和月份采集
                for year, months in self.selected_months.items():
                    if self.is_cancelled:
                        break
                    self._collect_year_data(year, months)
            
            # 创建DataFrame
            self.progress_signal.emit(f"采集完成，共 {len(self.catalog_data)} 条数据，正在生成表格...")
            df = pd.DataFrame(self.catalog_data, columns=columns)
            
            # 发送完成信号
            self.completed_signal.emit(df)
            
        except Exception as e:
            self.error_signal.emit(f"采集目录时出错: {str(e)}")
            import traceback
            self.progress_signal.emit(f"错误详情: {traceback.format_exc()}")
    
    def _collect_year_data(self, year, months=None):
        """采集指定年份的数据"""
        if self.is_cancelled:
            return
            
        year_url = urllib.parse.urljoin(self.base_url, f"{year}/")
        self.progress_signal.emit(f"正在采集 {year} 年的数据...")
        
        # 获取年份目录
        try:
            # 增加超时时间和重试机制
            max_retries = 3
            for retry in range(max_retries):
                try:
                    year_response = requests.get(year_url, timeout=30)
                    break
                except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
                    if retry < max_retries - 1:
                        self.progress_signal.emit(f"连接超时，正在重试 ({retry+1}/{max_retries})...")
                        time.sleep(2)  # 等待2秒后重试
                    else:
                        raise e
            
            if year_response.status_code != 200:
                self.progress_signal.emit(f"无法访问 {year} 年数据，跳过...")
                return
            
            year_soup = BeautifulSoup(year_response.text, 'html.parser')
            
            # 遍历月份目录
            for month_link in year_soup.find_all('a'):
                if self.is_cancelled:
                    break
                    
                month_href = month_link.get('href')
                if not month_href or not month_href.strip('/').isdigit():
                    continue
                
                month = month_href.strip('/')
                
                # 如果指定了月份，则只采集指定的月份
                if months and month not in months:
                    continue
                    
                self._collect_month_data(year, month)
                
        except Exception as e:
            self.progress_signal.emit(f"采集年份 {year} 时出错: {str(e)}")
    
    def _collect_month_data(self, year, month):
        """采集指定年份和月份的数据"""
        if self.is_cancelled:
            return
            
        month_url = urllib.parse.urljoin(self.base_url, f"{year}/{month}/")
        self.progress_signal.emit(f"正在采集 {year} 年 {month} 月的数据...")
        
        # 获取月份目录
        try:
            # 增加超时时间和重试机制
            max_retries = 3
            for retry in range(max_retries):
                try:
                    month_response = requests.get(month_url, timeout=30)
                    break
                except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
                    if retry < max_retries - 1:
                        self.progress_signal.emit(f"连接超时，正在重试 ({retry+1}/{max_retries})...")
                        time.sleep(2)  # 等待2秒后重试
                    else:
                        raise e
            
            if month_response.status_code != 200:
                self.progress_signal.emit(f"无法访问 {year} 年 {month} 月数据，跳过...")
                return
            
            month_soup = BeautifulSoup(month_response.text, 'html.parser')
            
            # 遍历日期目录
            for day_link in month_soup.find_all('a'):
                if self.is_cancelled:
                    break
                    
                day_href = day_link.get('href')
                if not day_href or not day_href.strip('/').isdigit():
                    continue
                
                day = day_href.strip('/')
                self._collect_day_data(year, month, day)
                
        except Exception as e:
            self.progress_signal.emit(f"采集月份 {month} 时出错: {str(e)}")
    
    def _collect_day_data(self, year, month, day):
        """采集指定年份、月份和日期的数据"""
        if self.is_cancelled:
            return
            
        day_url = urllib.parse.urljoin(self.base_url, f"{year}/{month}/{day}/")
        self.progress_signal.emit(f"正在采集 {year} 年 {month} 月 {day} 日的数据...")
        
        # 获取日期目录
        try:
            # 增加超时时间和重试机制
            max_retries = 3
            for retry in range(max_retries):
                try:
                    day_response = requests.get(day_url, timeout=30)
                    break
                except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
                    if retry < max_retries - 1:
                        self.progress_signal.emit(f"连接超时，正在重试 ({retry+1}/{max_retries})...")
                        time.sleep(2)  # 等待2秒后重试
                    else:
                        raise e
            
            if day_response.status_code != 200:
                self.progress_signal.emit(f"无法访问 {year} 年 {month} 月 {day} 日数据，跳过...")
                return
            
            day_soup = BeautifulSoup(day_response.text, 'html.parser')
            
            # 遍历图片文件夹
            for folder_link in day_soup.find_all('a'):
                if self.is_cancelled:
                    break
                    
                folder_href = folder_link.get('href')
                if not folder_href or folder_href == '../':
                    continue
                
                folder_name = folder_href.strip('/')
                self._collect_folder_data(year, month, day, folder_name)
                
        except Exception as e:
            self.progress_signal.emit(f"采集日期 {day} 时出错: {str(e)}")
    
    def _collect_folder_data(self, year, month, day, folder_name):
        """采集指定文件夹的数据"""
        if self.is_cancelled:
            return
            
        folder_url = urllib.parse.urljoin(self.base_url, f"{year}/{month}/{day}/{folder_name}/")
        self.progress_signal.emit(f"正在采集文件夹 {year}/{month}/{day}/{folder_name}/...")
        
        try:
            # 增加超时时间和重试机制
            max_retries = 3
            for retry in range(max_retries):
                try:
                    folder_response = requests.get(folder_url, timeout=30)
                    break
                except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
                    if retry < max_retries - 1:
                        self.progress_signal.emit(f"连接超时，正在重试 ({retry+1}/{max_retries})...")
                        time.sleep(2)  # 等待2秒后重试
                    else:
                        raise e
            
            if folder_response.status_code != 200:
                self.progress_signal.emit(f"无法访问 {folder_url}，跳过...")
                return
            
            folder_soup = BeautifulSoup(folder_response.text, 'html.parser')
            
            # 遍历文件
            files_found = 0
            for file_link in folder_soup.find_all('a'):
                if self.is_cancelled:
                    break
                    
                file_href = file_link.get('href')
                if not file_href or file_href == '../':
                    continue
                
                # 获取文件名，将%20转换为空格
                file_name = urllib.parse.unquote(file_href)
                
                # 获取文件大小和更新时间
                file_size = ""
                update_time = ""
                
                # 安全地获取文件大小
                next_sibling = file_link.next_sibling
                if next_sibling and isinstance(next_sibling, str):
                    file_size = next_sibling.strip()
                elif next_sibling and hasattr(next_sibling, 'string') and next_sibling.string:
                    file_size = next_sibling.string.strip()
                
                # 安全地获取更新时间
                if next_sibling and hasattr(next_sibling, 'next_sibling'):
                    next_next_sibling = next_sibling.next_sibling
                    if next_next_sibling and isinstance(next_next_sibling, str):
                        update_time = next_next_sibling.strip()
                    elif next_next_sibling and hasattr(next_next_sibling, 'string') and next_next_sibling.string:
                        update_time = next_next_sibling.string.strip()
                
                # 构建完整URL
                file_url = urllib.parse.urljoin(folder_url, file_href)
                
                # 添加到数据列表
                self.catalog_data.append([
                    year, month, day, folder_name, file_name, file_size, file_url, update_time
                ])
                
                files_found += 1
                
                # 每10个文件输出一次进度
                if files_found % 10 == 0:
                    self.progress_signal.emit(f"已采集 {files_found} 个文件...")
            
            self.progress_signal.emit(f"文件夹 {folder_name} 采集完成，共 {files_found} 个文件")
                
        except Exception as e:
            self.progress_signal.emit(f"采集文件夹 {folder_name} 时出错: {str(e)}")
            import traceback
            self.progress_signal.emit(f"错误详情: {traceback.format_exc()}")

class SettingsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.config = configparser.ConfigParser()
        self.config.read('config.ini')
        if not self.config.has_section('Settings'):
            self.config.add_section('Settings')
        
        # 设置窗口标题和大小
        self.setWindowTitle("应用设置")
        self.resize(500, 350)  # 增加高度以容纳更多设置项
        
        # 创建主布局
        layout = QVBoxLayout(self)
        
        # 路径设置
        path_group = QGroupBox("路径设置")
        path_layout = QFormLayout()
        
        # 添加小行星数据库路径设置
        self.asteroid_db_path = QLineEdit()
        self.asteroid_db_button = QPushButton("浏览...")
        self.asteroid_db_button.clicked.connect(self.browse_asteroid_db)
        asteroid_layout = QHBoxLayout()
        asteroid_layout.addWidget(self.asteroid_db_path)
        asteroid_layout.addWidget(self.asteroid_db_button)
        path_layout.addRow("小行星数据库路径:", asteroid_layout)
        
        # 添加输出路径设置
        self.output_path = QLineEdit()
        self.output_button = QPushButton("浏览...")
        self.output_button.clicked.connect(self.browse_output)
        output_layout = QHBoxLayout()
        output_layout.addWidget(self.output_path)
        output_layout.addWidget(self.output_button)
        path_layout.addRow("输出路径:", output_layout)
        
        self.table_path = QLineEdit()
        self.table_button = QPushButton("浏览...")
        self.table_button.clicked.connect(self.browse_table)
        table_layout = QHBoxLayout()
        table_layout.addWidget(self.table_path)
        table_layout.addWidget(self.table_button)
        path_layout.addRow("采集表路径:", table_layout)
        
        self.download_path = QLineEdit()
        self.download_button = QPushButton("浏览...")
        self.download_button.clicked.connect(self.browse_download)
        download_layout = QHBoxLayout()
        download_layout.addWidget(self.download_path)
        download_layout.addWidget(self.download_button)
        path_layout.addRow("下载路径:", download_layout)
        
        path_group.setLayout(path_layout)
        layout.addWidget(path_group)
        
        # 天文观测参数设置
        observatory_group = QGroupBox("观测站参数")
        observatory_layout = QFormLayout()
        
        # 添加盲解API密钥
        self.astrometry_api_key = QLineEdit()
        observatory_layout.addRow("盲解API密钥:", self.astrometry_api_key)
        
        # 添加WCS自动解析选项
        self.auto_parse_wcs = QCheckBox("启用WCS自动解析")
        self.auto_parse_wcs.setToolTip("启用后自动解析FITS文件中的WCS信息，禁用可提高大型图片加载速度")
        observatory_layout.addRow("WCS设置:", self.auto_parse_wcs)

        # 小行星导入极限星等设置
        self.asteroid_mag_limit = QDoubleSpinBox()
        self.asteroid_mag_limit.setDecimals(1)
        self.asteroid_mag_limit.setRange(5.0, 25.0)
        self.asteroid_mag_limit.setSingleStep(0.1)
        self.asteroid_mag_limit.setToolTip("仅标注不比该星等更暗的小行星 (值越小显示越少)")
        observatory_layout.addRow("小行星极限星等:", self.asteroid_mag_limit)
        
        observatory_group.setLayout(observatory_layout)
        layout.addWidget(observatory_group)
        
        # 作者信息
        author_label = QLabel("作者：Guoyou Sun(孙国佑)")
        author_label.setAlignment(Qt.AlignCenter)
        font = author_label.font()
        font.setBold(True)
        author_label.setFont(font)
        layout.addWidget(author_label)
        
        # 按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)
        
        # 加载当前设置
        self.load_current_settings()
    
    def browse_table(self):
        directory = QFileDialog.getExistingDirectory(self, "选择采集表文件夹")
        if directory:
            self.table_path.setText(directory)
            # 保存到配置
            if not self.config.has_section("Settings"):
                self.config.add_section("Settings")
            self.config["Settings"]["table_path"] = directory
            with open('config.ini', 'w') as f:
                self.config.write(f)
    
    def browse_download(self):
        directory = QFileDialog.getExistingDirectory(self, "选择下载目录")
        if directory:
            self.download_path.setText(directory)
            # 保存到配置
            if not self.config.has_section("Settings"):
                self.config.add_section("Settings")
            self.config["Settings"]["download_path"] = directory
            with open('config.ini', 'w') as f:
                self.config.write(f)
    
    def browse_asteroid_db(self):
        directory = QFileDialog.getExistingDirectory(self, "选择小行星数据库文件夹")
        if directory:
            self.asteroid_db_path.setText(directory)
            # 保存到配置
            if not self.config.has_section("Settings"):
                self.config.add_section("Settings")
            self.config["Settings"]["asteroid_db_path"] = directory
            with open('config.ini', 'w') as f:
                self.config.write(f)
    
    def browse_output(self):
        directory = QFileDialog.getExistingDirectory(self, "选择输出目录")
        if directory:
            self.output_path.setText(directory)
            # 保存到配置
            if not self.config.has_section("Settings"):
                self.config.add_section("Settings")
            self.config["Settings"]["output_path"] = directory
            with open('config.ini', 'w') as f:
                self.config.write(f)
    
    def load_current_settings(self):
        """加载当前设置"""
        try:
            # 加载路径设置
            self.table_path.setText(self.config.get('Settings', 'table_path', fallback=""))
            self.download_path.setText(self.config.get('Settings', 'download_path', fallback=""))
            self.asteroid_db_path.setText(self.config.get('Settings', 'asteroid_db_path', fallback=""))
            self.output_path.setText(self.config.get('Settings', 'output_path', fallback=""))
            
            # 加载观测站参数
            self.astrometry_api_key.setText(self.config.get('Settings', 'astrometry_api_key', fallback=""))
            # 加载WCS自动解析选项
            try:
                auto_flag = self.config.get('Settings', 'auto_parse_wcs', fallback="true").strip().lower()
                self.auto_parse_wcs.setChecked(auto_flag in ("true", "1", "yes"))
            except Exception:
                self.auto_parse_wcs.setChecked(False)
            try:
                mag_limit_str = self.config.get('Settings', 'asteroid_mag_limit', fallback="21")
                self.asteroid_mag_limit.setValue(float(mag_limit_str))
            except Exception:
                self.asteroid_mag_limit.setValue(21.0)
        except Exception as e:
            print(f"加载设置时出错: {e}")
    
    def accept(self):
        """保存设置并关闭对话框"""
        try:
            # 保存路径设置
            if not self.config.has_section("Settings"):
                self.config.add_section("Settings")
            self.config["Settings"]["table_path"] = self.table_path.text()
            self.config["Settings"]["download_path"] = self.download_path.text()
            self.config["Settings"]["asteroid_db_path"] = self.asteroid_db_path.text()
            self.config["Settings"]["output_path"] = self.output_path.text()
            
            # 保存观测站参数
            self.config["Settings"]["astrometry_api_key"] = self.astrometry_api_key.text()
            # 保存WCS自动解析选项
            self.config["Settings"]["auto_parse_wcs"] = "true" if self.auto_parse_wcs.isChecked() else "false"
            self.config["Settings"]["asteroid_mag_limit"] = f"{self.asteroid_mag_limit.value():.1f}"
            
            # 写入配置文件
            with open('config.ini', 'w') as f:
                self.config.write(f)
            
            # 调用父类的accept方法关闭对话框
            super().accept()
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存设置时出错: {e}")
            print(f"保存设置时出错: {e}")


class OnlineImageSearchThread(QThread):
    """在线图像搜索线程"""
    found_signal = pyqtSignal(list)  # 找到图像的信号，返回图像列表 [(filename, url, date_info), ...]
    error_signal = pyqtSignal(str)  # 错误信号
    progress_signal = pyqtSignal(str)  # 进度信号
    
    def __init__(self, table_path, search_condition, search_type):
        super().__init__()
        self.table_path = table_path
        self.search_condition = search_condition
        self.search_type = search_type  # "time", "catalog" 或 "filename"
        self.is_cancelled = False
        self.year = None
        self.month = None
        self.day = None
        self.folder = None
        self.filename = None
        
    def run(self):
        """运行搜索线程"""
        self.is_cancelled = False
        
        try:
            self.progress_signal.emit(f"开始搜索...")
            
            if self.search_type == "time":
                # 按时间搜索，只需要年份和月份
                self.search_by_time()
            elif self.search_type == "catalog":
                # 从采集表格搜索
                self.search_from_catalog()
            else:
                self.error_signal.emit(f"未知的搜索类型: {self.search_type}")
                
        except Exception as e:
            self.error_signal.emit(f"搜索出错: {str(e)}")
            import traceback
            self.progress_signal.emit(f"错误详情: {traceback.format_exc()}")
    
    def search_by_time(self):
        """按时间搜索，查找对应时间的采集表格"""
        try:
            if not self.year:
                self.error_signal.emit("未指定年份")
                return
                
            self.progress_signal.emit(f"搜索条件: 年份={self.year}, 月份={self.month}")
            if hasattr(self, 'filename') and self.filename:
                self.progress_signal.emit(f"同时按图片名搜索: {self.filename}")
            
            # 确保表格路径存在
            if not os.path.exists(self.table_path):
                self.error_signal.emit(f"表格路径不存在: {self.table_path}")
                return
            
            # 查找所有CSV文件
            csv_files = glob.glob(os.path.join(self.table_path, "**/*.csv"), recursive=True)
            if not csv_files:
                self.error_signal.emit(f"未找到采集表格文件")
                return
            
            # 搜索结果
            found_images = []
            
            # 构建文件夹名称，例如 202503 表示2025年3月
            folder_name = f"{self.year}{self.month}" if self.month != "全部" else self.year
            self.progress_signal.emit(f"搜索文件夹: {folder_name}")
            
            # 遍历所有CSV文件
            for csv_file in csv_files:
                if self.is_cancelled:
                    break
                    
                self.progress_signal.emit(f"正在搜索表格: {os.path.basename(csv_file)}")
                
                try:
                    # 读取CSV文件
                    df = pd.read_csv(csv_file, encoding="utf-8-sig")
                    
                    # 检查必要的列是否存在
                    required_columns = ["文件名", "URL"]
                    if not all(col in df.columns for col in required_columns):
                        self.progress_signal.emit(f"表格 {os.path.basename(csv_file)} 缺少必要的列")
                        continue
                    
                    # 按年份和月份筛选
                    filtered_df = df
                    if "年份" in df.columns and self.year != "全部":
                        filtered_df = filtered_df[filtered_df["年份"].astype(str) == self.year]
                    if "月份" in df.columns and self.month != "全部":
                        filtered_df = filtered_df[filtered_df["月份"].astype(str) == self.month]
                    
                    # 如果有指定文件名，也按文件名筛选
                    if hasattr(self, 'filename') and self.filename:
                        # 先尝试精确匹配
                        exact_match = filtered_df[filtered_df["文件名"].str.contains(f"^{re.escape(self.filename)}$", case=False, regex=True, na=False)]
                        if not exact_match.empty:
                            self.progress_signal.emit(f"在表格 {os.path.basename(csv_file)} 中找到精确匹配")
                            filtered_df = exact_match
                        else:
                            # 如果没有精确匹配，尝试模糊匹配
                            name_filtered_df = filtered_df[filtered_df["文件名"].str.contains(re.escape(self.filename), case=False, regex=True, na=False)]
                            if not name_filtered_df.empty:
                                self.progress_signal.emit(f"在表格 {os.path.basename(csv_file)} 中找到 {len(name_filtered_df)} 条记录")
                                filtered_df = name_filtered_df
                            else:
                                # 如果没有匹配，跳过当前文件
                                continue
                    
                    # 获取所有图像文件名和URL
                    for _, row in filtered_df.iterrows():
                        if self.is_cancelled:
                            break
                            
                        file_name = row["文件名"]
                        file_url = row["URL"]
                        
                        # 将文件名中的%20转换为空格
                        decoded_file_name = urllib.parse.unquote(file_name)
                        
                        # 构建时间信息
                        date_info = ""
                        if "年份" in row and "月份" in row and "日期" in row:
                            date_info = f"{row['年份']}-{row['月份']}-{row['日期']}"
                        elif "年份" in row and "月份" in row:
                            date_info = f"{row['年份']}-{row['月份']}"
                        
                        # 添加到结果列表
                        found_images.append((decoded_file_name, file_url, date_info))
                    
                except Exception as e:
                    self.progress_signal.emit(f"读取表格 {os.path.basename(csv_file)} 出错: {str(e)}")
                    continue
            
            # 发送结果
            self.progress_signal.emit(f"搜索完成，共找到 {len(found_images)} 个文件")
            self.found_signal.emit(found_images)
            
        except Exception as e:
            self.error_signal.emit(f"按时间搜索出错: {str(e)}")
    
    def search_from_catalog(self):
        """从采集表格搜索"""
        try:
            # 确保表格路径存在
            if not os.path.exists(self.table_path):
                self.error_signal.emit(f"表格路径不存在: {self.table_path}")
                return
            
            # 查找所有CSV文件
            csv_files = glob.glob(os.path.join(self.table_path, "**/*.csv"), recursive=True)
            if not csv_files:
                self.error_signal.emit(f"未找到采集表格文件")
                return
            
            # 搜索结果
            found_images = []
            
            # 显示搜索条件
            self.progress_signal.emit(f"搜索所有CSV文件")
            
            # 如果有指定文件名，显示文件名搜索条件
            if hasattr(self, 'filename') and self.filename:
                self.progress_signal.emit(f"按图片名搜索: {self.filename}")
            else:
                self.progress_signal.emit(f"搜索所有图片")
            
            # 遍历所有CSV文件
            for csv_file in csv_files:
                if self.is_cancelled:
                    break
                    
                self.progress_signal.emit(f"正在搜索表格: {os.path.basename(csv_file)}")
                
                try:
                    # 读取CSV文件
                    df = pd.read_csv(csv_file, encoding="utf-8-sig")
                    
                    # 检查必要的列是否存在
                    required_columns = ["文件名", "URL"]
                    if not all(col in df.columns for col in required_columns):
                        self.progress_signal.emit(f"表格 {os.path.basename(csv_file)} 缺少必要的列")
                        continue
                    
                    # 初始化筛选后的数据框
                    filtered_df = df
                    
                    # 如果有指定文件名，按文件名筛选
                    if hasattr(self, 'filename') and self.filename:
                        # 先尝试精确匹配
                        exact_match = filtered_df[filtered_df["文件名"].str.contains(f"^{re.escape(self.filename)}$", case=False, regex=True, na=False)]
                        if not exact_match.empty:
                            self.progress_signal.emit(f"在表格 {os.path.basename(csv_file)} 中找到精确匹配")
                            filtered_df = exact_match
                        else:
                            # 如果没有精确匹配，尝试模糊匹配
                            name_filtered_df = filtered_df[filtered_df["文件名"].str.contains(re.escape(self.filename), case=False, regex=True, na=False)]
                            if not name_filtered_df.empty:
                                self.progress_signal.emit(f"在表格 {os.path.basename(csv_file)} 中找到 {len(name_filtered_df)} 条记录")
                                filtered_df = name_filtered_df
                            else:
                                # 如果没有匹配，跳过当前文件
                                continue
                    
                    # 如果有结果，添加到搜索结果中
                    if not filtered_df.empty:
                        self.progress_signal.emit(f"在表格 {os.path.basename(csv_file)} 中找到 {len(filtered_df)} 条记录")
                        for _, row in filtered_df.iterrows():
                            # 获取文件名和URL
                            file_name = row["文件名"]
                            file_url = row["URL"]
                            
                            # 将文件名中的%20转换为空格
                            decoded_file_name = urllib.parse.unquote(file_name)
                            
                            # 构建时间信息
                            date_info = ""
                            if "年份" in row and "月份" in row and "日期" in row:
                                date_info = f"{row['年份']}-{row['月份']}-{row['日期']}"
                            elif "年份" in row and "月份" in row:
                                date_info = f"{row['年份']}-{row['月份']}"
                            
                            # 添加到结果列表
                            found_images.append((decoded_file_name, file_url, date_info))
                            
                except Exception as e:
                    self.progress_signal.emit(f"读取表格 {os.path.basename(csv_file)} 出错: {str(e)}")
                    continue
            
            # 发送结果
            self.progress_signal.emit(f"搜索完成，共找到 {len(found_images)} 个文件")
            self.found_signal.emit(found_images)
        
        except Exception as e:
            self.error_signal.emit(f"从采集表格搜索出错: {str(e)}")


class AutoParseThread(QThread):
    progress_signal = pyqtSignal(str)
    result_signal = pyqtSignal(dict)
    error_signal = pyqtSignal(str)
    def __init__(self, image_processor):
        super().__init__()
        self.image_processor = image_processor
    def run(self):
        try:
            from coordinate_parser import CoordinateParser
            from app import SettingsManager
            settings_manager = SettingsManager()
            coord_parser = CoordinateParser(settings_manager)
            def cb(msg):
                self.progress_signal.emit(msg)
            result = coord_parser.parse_image_coordinates(self.image_processor, cb)
            if not result:
                self.result_signal.emit({})
            else:
                self.result_signal.emit(result)
        except Exception as e:
            self.error_signal.emit(str(e))

class AsteroidImportThread(QThread):
    progress_signal = pyqtSignal(str)
    result_signal = pyqtSignal(list)
    error_signal = pyqtSignal(str)
    def __init__(self, mpcorb_path, obs_time, center_ra, center_dec, fov_w_deg, fov_h_deg, wcs, mag_limit=None):
        super().__init__()
        self.mpcorb_path = mpcorb_path
        self.obs_time = obs_time
        self.center_ra = center_ra
        self.center_dec = center_dec
        self.fov_w_deg = fov_w_deg
        self.fov_h_deg = fov_h_deg
        self.wcs = wcs
        self.mag_limit = mag_limit
    def _unpack_packed_date(self, s):
        if not s or len(s) < 5:
            return None
        c1 = s[0]
        yy = int(s[1:3])
        if c1 == 'I':
            year = 1800 + yy
        elif c1 == 'J':
            year = 1900 + yy
        elif c1 == 'K':
            year = 2000 + yy
        else:
            return None
        def decode_md(ch):
            if ch.isdigit():
                return int(ch)
            table = {
                'A':10,'B':11,'C':12,'D':13,'E':14,'F':15,'G':16,'H':17,'I':18,'J':19,
                'K':20,'L':21,'M':22,'N':23,'O':24,'P':25,'Q':26,'R':27,'S':28,'T':29,
                'U':30,'V':31
            }
            return table.get(ch, 1)
        month = decode_md(s[3])
        day = decode_md(s[4])
        try:
            return datetime(year, month, day)
        except Exception:
            return None
    def _parse_line(self, line):
        if len(line) < 160:
            return None
        try:
            packed_id = line[0:7].strip()
            H = float(line[8:13].strip()) if line[8:13].strip() else None
            G = float(line[14:19].strip()) if line[14:19].strip() else 0.15
            epoch_packed = line[20:25].strip()
            M0 = float(line[26:35].strip())
            w = float(line[37:46].strip())
            Omega = float(line[48:57].strip())
            inc = float(line[59:68].strip())
            e = float(line[70:79].strip())
            n = float(line[80:91].strip())
            a = float(line[92:103].strip())
            name = line[166:194].strip()
            return {
                'id': packed_id,
                'name': name,
                'H': H,
                'G': G,
                'epoch_packed': epoch_packed,
                'M0': M0,
                'w': w,
                'Omega': Omega,
                'i': inc,
                'e': e,
                'n': n,
                'a': a
            }
        except Exception:
            return None
    def _kepler_position(self, elems, dt_days):
        e = elems['e']
        a = elems['a']
        M = np.deg2rad((elems['M0'] + elems['n'] * dt_days) % 360.0)
        E = M
        for _ in range(15):
            f = E - e * np.sin(E) - M
            fp = 1 - e * np.cos(E)
            dE = -f / fp
            E += dE
            if abs(dE) < 1e-10:
                break
        nu = 2 * np.arctan2(np.sqrt(1 + e) * np.sin(E / 2), np.sqrt(1 - e) * np.cos(E / 2))
        r = a * (1 - e * np.cos(E))
        x_orb = r * np.cos(nu)
        y_orb = r * np.sin(nu)
        z_orb = 0.0
        w = np.deg2rad(elems['w'])
        Omega = np.deg2rad(elems['Omega'])
        inc = np.deg2rad(elems['i'])
        cosw, sinw = np.cos(w), np.sin(w)
        cosO, sinO = np.cos(Omega), np.sin(Omega)
        cosi, sini = np.cos(inc), np.sin(inc)
        x1 = cosw * x_orb - sinw * y_orb
        y1 = sinw * x_orb + cosw * y_orb
        z1 = z_orb
        x2 = x1
        y2 = cosi * y1
        z2 = sini * y1
        xe = cosO * x2 - sinO * y2
        ye = sinO * x2 + cosO * y2
        ze = z2
        eps = np.deg2rad(23.43928)
        xq = xe
        yq = np.cos(eps) * ye - np.sin(eps) * ze
        zq = np.sin(eps) * ye + np.cos(eps) * ze
        return np.array([xq, yq, zq]), r
    def _hg_magnitude(self, H, G, r, delta, phase_deg):
        if H is None:
            return None
        alpha = np.deg2rad(phase_deg)
        tan_a2 = np.tan(alpha / 2)
        phi1 = np.exp(-3.33 * np.power(tan_a2, 0.63))
        phi2 = np.exp(-1.87 * np.power(tan_a2, 1.22))
        return H + 5 * np.log10(max(r, 1e-6) * max(delta, 1e-6)) - 2.5 * np.log10((1 - G) * phi1 + G * phi2)
    def _process_chunk_local(self, chunk_lines, ex, ey, ez, cx, cy, rad):
        out = []
        for ln in chunk_lines:
            d = self._parse_line(ln)
            if not d:
                continue
            epoch_dt = self._unpack_packed_date(d['epoch_packed'])
            if not epoch_dt:
                continue
            dt_days = (self.obs_time - epoch_dt).days + (self.obs_time - epoch_dt).seconds / 86400.0
            pos_vec, r = self._kepler_position(d, dt_days)
            gx = pos_vec[0] - ex
            gy = pos_vec[1] - ey
            gz = pos_vec[2] - ez
            delta = np.sqrt(gx * gx + gy * gy + gz * gz)
            ra = (np.degrees(np.arctan2(gy, gx)) + 360.0) % 360.0
            dec = np.degrees(np.arctan2(gz, np.sqrt(gx * gx + gy * gy)))
            dra = (ra - cx + 540.0) % 360.0 - 180.0
            dec1 = np.deg2rad(cy)
            dec2 = np.deg2rad(dec)
            dra_rad = np.deg2rad(dra)
            hav = np.sin((dec2 - dec1) / 2.0) ** 2 + np.cos(dec1) * np.cos(dec2) * np.sin(dra_rad / 2.0) ** 2
            ang_sep = np.degrees(2.0 * np.arcsin(np.sqrt(np.clip(hav, 0.0, 1.0))))
            if ang_sep <= rad:
                try:
                    ae = np.array([-gx, -gy, -gz])  # 小行星->地球
                    asun = -pos_vec                 # 小行星->太阳
                    phase = np.degrees(np.arccos(np.clip(np.dot(asun, ae) / (np.linalg.norm(pos_vec) * delta), -1.0, 1.0)))
                    mag = self._hg_magnitude(d['H'], d['G'], r, delta, phase)
                    if self.mag_limit is not None and mag is not None and mag > self.mag_limit:
                        continue
                    label = d['name'] if d['name'] else d['id']
                    if mag is not None:
                        label = f"{label} V{mag:.1f}"
                    out.append({'ra': float(ra), 'dec': float(dec), 'label': label})
                except Exception:
                    pass
        return out
    def run(self):
        try:
            if not os.path.isfile(self.mpcorb_path):
                self.error_signal.emit("未找到MPCORB.DAT")
                return
            t_obs = Time(self.obs_time)
            solar_system_ephemeris.set('builtin')
            earth_vec = get_body_barycentric('earth', t_obs)
            sun_vec = get_body_barycentric('sun', t_obs)
            helio = earth_vec - sun_vec
            ex = helio.x.to(u.AU).value
            ey = helio.y.to(u.AU).value
            ez = helio.z.to(u.AU).value
            with open(self.mpcorb_path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()
            res = []
            cx = self.center_ra
            cy = self.center_dec
            rad = 0.5 * np.sqrt(self.fov_w_deg * self.fov_w_deg + self.fov_h_deg * self.fov_h_deg)
            total = len(lines)
            chunk_size = 50000
            workers = max(2, min(8, (os.cpu_count() or 4)))
            try:
                with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as pool:
                    futures = []
                    for i in range(0, total, chunk_size):
                        chunk = lines[i:i+chunk_size]
                        futures.append(pool.submit(asteroid_worker.process_chunk, chunk, ex, ey, ez, cx, cy, rad, self.obs_time.timestamp(), self.mag_limit))
                    done = 0
                    candidates = []
                    for fut in concurrent.futures.as_completed(futures):
                        try:
                            candidates.extend(fut.result())
                        except Exception:
                            pass
                        done += 1
                        self.progress_signal.emit(f"正在导入: {min(done*chunk_size, total)}/{total}")
            except Exception:
                candidates = self._process_chunk_local(lines, ex, ey, ez, cx, cy, rad)
            if candidates:
                ras = [c['ra'] for c in candidates]
                decs = [c['dec'] for c in candidates]
                sc = SkyCoord(ras * u.deg, decs * u.deg)
                xs, ys = self.wcs.world_to_pixel(sc)
                res_markers = []
                for i in range(len(ras)):
                    label = candidates[i]['label']
                    res_markers.append({'x': float(xs[i]), 'y': float(ys[i]), 'type': 'asteroid', 'label': label})
                self.result_signal.emit(res_markers)
            else:
                self.result_signal.emit([])
        except Exception as e:
            self.error_signal.emit(str(e))

    @staticmethod
    def _static_unpack_packed_date(s):
        if not s or len(s) < 5:
            return None
        c1 = s[0]
        yy = int(s[1:3])
        if c1 == 'I':
            year = 1800 + yy
        elif c1 == 'J':
            year = 1900 + yy
        elif c1 == 'K':
            year = 2000 + yy
        else:
            return None
        def decode_md(ch):
            if ch.isdigit():
                return int(ch)
            table = {
                'A':10,'B':11,'C':12,'D':13,'E':14,'F':15,'G':16,'H':17,'I':18,'J':19,
                'K':20,'L':21,'M':22,'N':23,'O':24,'P':25,'Q':26,'R':27,'S':28,'T':29,
                'U':30,'V':31
            }
            return table.get(ch, 1)
        month = decode_md(s[3])
        day = decode_md(s[4])
        try:
            return datetime(year, month, day)
        except Exception:
            return None

    @staticmethod
    def _static_parse_line(line):
        if len(line) < 160:
            return None
        try:
            packed_id = line[0:7].strip()
            H = float(line[8:13].strip()) if line[8:13].strip() else None
            G = float(line[14:19].strip()) if line[14:19].strip() else 0.15
            epoch_packed = line[20:25].strip()
            M0 = float(line[26:35].strip())
            w = float(line[37:46].strip())
            Omega = float(line[48:57].strip())
            inc = float(line[59:68].strip())
            e = float(line[70:79].strip())
            n = float(line[80:91].strip())
            a = float(line[92:103].strip())
            name = line[166:194].strip()
            return {
                'id': packed_id,
                'name': name,
                'H': H,
                'G': G,
                'epoch_packed': epoch_packed,
                'M0': M0,
                'w': w,
                'Omega': Omega,
                'i': inc,
                'e': e,
                'n': n,
                'a': a
            }
        except Exception:
            return None

    @staticmethod
    def _static_kepler_position(elems, dt_days):
        e = elems['e']
        a = elems['a']
        M = np.deg2rad((elems['M0'] + elems['n'] * dt_days) % 360.0)
        E = M
        for _ in range(15):
            f = E - e * np.sin(E) - M
            fp = 1 - e * np.cos(E)
            dE = -f / fp
            E += dE
            if abs(dE) < 1e-10:
                break
        nu = 2 * np.arctan2(np.sqrt(1 + e) * np.sin(E / 2), np.sqrt(1 - e) * np.cos(E / 2))
        r = a * (1 - e * np.cos(E))
        x_orb = r * np.cos(nu)
        y_orb = r * np.sin(nu)
        z_orb = 0.0
        w = np.deg2rad(elems['w'])
        Omega = np.deg2rad(elems['Omega'])
        inc = np.deg2rad(elems['i'])
        cosw, sinw = np.cos(w), np.sin(w)
        cosO, sinO = np.cos(Omega), np.sin(Omega)
        cosi, sini = np.cos(inc), np.sin(inc)
        x1 = cosw * x_orb - sinw * y_orb
        y1 = sinw * x_orb + cosw * y_orb
        z1 = z_orb
        x2 = x1
        y2 = cosi * y1
        z2 = sini * y1
        xe = cosO * x2 - sinO * y2
        ye = sinO * x2 + cosO * y2
        ze = z2
        eps = np.deg2rad(23.43928)
        xq = xe
        yq = np.cos(eps) * ye - np.sin(eps) * ze
        zq = np.sin(eps) * ye + np.cos(eps) * ze
        return np.array([xq, yq, zq]), r

    @staticmethod
    def _static_hg_magnitude(H, G, r, delta, phase_deg):
        if H is None:
            return None
        alpha = np.deg2rad(phase_deg)
        tan_a2 = np.tan(alpha / 2)
        phi1 = np.exp(-3.33 * np.power(tan_a2, 0.63))
        phi2 = np.exp(-1.87 * np.power(tan_a2, 1.22))
        return H + 5 * np.log10(max(r, 1e-6) * max(delta, 1e-6)) - 2.5 * np.log10((1 - G) * phi1 + G * phi2)

    @staticmethod
    def _static_process_chunk(chunk_lines, ex, ey, ez, cx, cy, rad, obs_ts):
        out = []
        for ln in chunk_lines:
            d = AsteroidImportThread._static_parse_line(ln)
            if not d:
                continue
            epoch_dt = AsteroidImportThread._static_unpack_packed_date(d['epoch_packed'])
            if not epoch_dt:
                continue
            dt_days = (datetime.fromtimestamp(obs_ts) - epoch_dt).days + (datetime.fromtimestamp(obs_ts) - epoch_dt).seconds / 86400.0
            pos_vec, r = AsteroidImportThread._static_kepler_position(d, dt_days)
            gx = pos_vec[0] - ex
            gy = pos_vec[1] - ey
            gz = pos_vec[2] - ez
            delta = np.sqrt(gx * gx + gy * gy + gz * gz)
            ra = (np.degrees(np.arctan2(gy, gx)) + 360.0) % 360.0
            dec = np.degrees(np.arctan2(gz, np.sqrt(gx * gx + gy * gy)))
            dra = (ra - cx + 540.0) % 360.0 - 180.0
            ddec = dec - cy
            ang_sep = np.degrees(np.arccos(np.clip(np.cos(np.deg2rad(ddec)) * np.cos(np.deg2rad(dra)), -1.0, 1.0)))
            if ang_sep <= rad:
                try:
                    phase = np.degrees(np.arccos(np.clip(((-pos_vec).dot([gx, gy, gz])) / (np.linalg.norm(pos_vec) * delta), -1.0, 1.0)))
                    mag = AsteroidImportThread._static_hg_magnitude(d['H'], d['G'], r, delta, phase)
                    label = d['name'] if d['name'] else d['id']
                    if mag is not None:
                        label = f"{label} V{mag:.1f}"
                    out.append({'ra': float(ra), 'dec': float(dec), 'label': label})
                except Exception:
                    pass
        return out

class CometImportThread(QThread):
    progress_signal = pyqtSignal(str)
    result_signal = pyqtSignal(list)
    error_signal = pyqtSignal(str)
    def __init__(self, comet_path, obs_time, center_ra, center_dec, fov_w_deg, fov_h_deg, wcs, mag_limit=None):
        super().__init__()
        self.comet_path = comet_path
        self.obs_time = obs_time
        self.center_ra = center_ra
        self.center_dec = center_dec
        self.fov_w_deg = fov_w_deg
        self.fov_h_deg = fov_h_deg
        self.wcs = wcs
        self.mag_limit = mag_limit
    @staticmethod
    def _parse_month(m):
        m = m.strip()
        if not m:
            return None
        if m.isdigit():
            try:
                return int(m)
            except Exception:
                return None
        names = {'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6,'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12}
        key = m[:3].title()
        return names.get(key)
    @staticmethod
    def _static_parse_line(line):
        try:
            if not line or line.strip().startswith('#'):
                return None
            toks = re.findall(r'\S+', line.strip())
            idx = None
            year = None
            month = None
            day = None
            q = None
            e = None
            w = None
            Omega = None
            inc = None
            for j in range(max(0, len(toks) - 12)):
                t_year = toks[j]
                try:
                    yy = int(t_year)
                except Exception:
                    continue
                if yy < 1600 or yy > 2600:
                    continue
                m_tok = toks[j+1] if j+1 < len(toks) else None
                d_tok = toks[j+2] if j+2 < len(toks) else None
                if m_tok is None or d_tok is None:
                    continue
                try:
                    mm = int(m_tok)
                except Exception:
                    mm = CometImportThread._parse_month(m_tok)
                try:
                    dd = float(d_tok)
                except Exception:
                    continue
                if mm is None or mm < 1 or mm > 12:
                    continue
                if dd <= 0 or dd > 32:
                    continue
                def f(x):
                    try:
                        return float(x)
                    except Exception:
                        return None
                q1 = f(toks[j+3] if j+3 < len(toks) else None)
                e1 = f(toks[j+4] if j+4 < len(toks) else None)
                w1 = f(toks[j+5] if j+5 < len(toks) else None)
                O1 = f(toks[j+6] if j+6 < len(toks) else None)
                i1 = f(toks[j+7] if j+7 < len(toks) else None)
                if None in (q1, e1, w1, O1, i1):
                    continue
                if not (q1 > 0 and 0 <= e1 <= 3 and 0 <= w1 <= 360 and 0 <= O1 <= 360 and 0 <= i1 <= 180):
                    continue
                idx = j
                year = yy
                month = mm
                day = dd
                q = q1
                e = e1
                w = w1
                Omega = O1
                inc = i1
                break
            if idx is None:
                return None
            day_int = int(day)
            frac = day - day_int
            hour = int(frac * 24)
            minute = int((frac * 24 - hour) * 60)
            second = int((((frac * 24 - hour) * 60) - minute) * 60)
            T = datetime(year, month, day_int, hour, minute, second)
            name = "".join([t+' ' for t in toks[:idx]]).strip()
            if not name:
                name = "".join([t+' ' for t in toks[idx+8:]]).strip()
            return {'T': T, 'q': q, 'e': e, 'w': w, 'Omega': Omega, 'i': inc, 'name': name}
        except Exception:
            return None
    @staticmethod
    def _static_kepler_position(elems, dt_days):
        e = elems['e']
        q = elems['q']
        k = 0.01720209895
        if e < 1.0:
            a = q / max(1.0 - e, 1e-12)
            n_deg = (k * 180.0 / np.pi) / (a ** 1.5)
            M = np.deg2rad(n_deg * dt_days)
            E = M
            for _ in range(40):
                f = E - e * np.sin(E) - M
                fp = 1 - e * np.cos(E)
                dE = -f / fp
                E += dE
                if abs(dE) < 1e-12:
                    break
            nu = 2 * np.arctan2(np.sqrt(1 + e) * np.sin(E / 2), np.sqrt(1 - e) * np.cos(E / 2))
            r = a * (1 - e * np.cos(E))
        elif abs(e - 1.0) < 1e-6:
            W = (2.0 * dt_days) / (np.sqrt((q ** 3) / (k ** 2)) + 1e-12)
            D = np.cbrt(3.0 * W)
            for _ in range(40):
                f = D + (D ** 3) / 3.0 - W
                fp = 1.0 + D * D
                dD = -f / fp
                D += dD
                if abs(dD) < 1e-12:
                    break
            nu = 2.0 * np.arctan(D)
            r = q * (1.0 + D * D)
        else:
            a = q / (1.0 - e)
            ah = abs(a)
            n_deg = (k * 180.0 / np.pi) / (ah ** 1.5)
            M = np.deg2rad(n_deg * dt_days)
            H = np.arcsinh(M / max(e, 1e-12))
            for _ in range(60):
                f = e * np.sinh(H) - H - M
                fp = e * np.cosh(H) - 1.0
                dH = -f / fp
                H += dH
                if abs(dH) < 1e-12:
                    break
            nu = 2.0 * np.arctan(np.sqrt((e + 1.0) / (e - 1.0)) * np.tanh(H / 2.0))
            r = ah * (e * np.cosh(H) - 1.0)
        x_orb = r * np.cos(nu)
        y_orb = r * np.sin(nu)
        z_orb = 0.0
        w = np.deg2rad(elems['w'])
        Omega = np.deg2rad(elems['Omega'])
        inc = np.deg2rad(elems['i'])
        cosw, sinw = np.cos(w), np.sin(w)
        cosO, sinO = np.cos(Omega), np.sin(Omega)
        cosi, sini = np.cos(inc), np.sin(inc)
        x1 = cosw * x_orb - sinw * y_orb
        y1 = sinw * x_orb + cosw * y_orb
        z1 = z_orb
        x2 = x1
        y2 = cosi * y1
        z2 = sini * y1
        xe = cosO * x2 - sinO * y2
        ye = sinO * x2 + cosO * y2
        ze = z2
        eps = np.deg2rad(23.43928)
        xq = xe
        yq = np.cos(eps) * ye - np.sin(eps) * ze
        zq = np.sin(eps) * ye + np.cos(eps) * ze
        return np.array([xq, yq, zq]), r
    @staticmethod
    def _static_process_chunk(chunk_lines, ex, ey, ez, cx, cy, rad, obs_ts):
        out = []
        for ln in chunk_lines:
            d = CometImportThread._static_parse_line(ln)
            if not d:
                continue
            dt_days = (datetime.fromtimestamp(obs_ts) - d['T']).days + (datetime.fromtimestamp(obs_ts) - d['T']).seconds / 86400.0
            pos_r = CometImportThread._static_kepler_position(d, dt_days)
            if not pos_r or pos_r[0] is None:
                continue
            pos_vec, r = pos_r
            gx = pos_vec[0] - ex
            gy = pos_vec[1] - ey
            gz = pos_vec[2] - ez
            delta = np.sqrt(gx * gx + gy * gy + gz * gz)
            ra = (np.degrees(np.arctan2(gy, gx)) + 360.0) % 360.0
            dec = np.degrees(np.arctan2(gz, np.sqrt(gx * gx + gy * gy)))
            dra = (ra - cx + 540.0) % 360.0 - 180.0
            dec1 = np.deg2rad(cy)
            dec2 = np.deg2rad(dec)
            dra_rad = np.deg2rad(dra)
            hav = np.sin((dec2 - dec1) / 2.0) ** 2 + np.cos(dec1) * np.cos(dec2) * np.sin(dra_rad / 2.0) ** 2
            ang_sep = np.degrees(2.0 * np.arcsin(np.sqrt(np.clip(hav, 0.0, 1.0))))
            if ang_sep <= rad:
                try:
                    label = d['name']
                    out.append({'ra': float(ra), 'dec': float(dec), 'label': label})
                except Exception:
                    pass
        return out
    def run(self):
        try:
            if not os.path.isfile(self.comet_path):
                self.error_signal.emit("未找到CometEls.txt")
                return
            t_obs = Time(self.obs_time)
            solar_system_ephemeris.set('builtin')
            earth_vec = get_body_barycentric('earth', t_obs)
            sun_vec = get_body_barycentric('sun', t_obs)
            helio = earth_vec - sun_vec
            ex = helio.x.to(u.AU).value
            ey = helio.y.to(u.AU).value
            ez = helio.z.to(u.AU).value
            with open(self.comet_path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()
            cx = self.center_ra
            cy = self.center_dec
            rad = 0.5 * np.sqrt(self.fov_w_deg * self.fov_w_deg + self.fov_h_deg * self.fov_h_deg)
            total = len(lines)
            chunk_size = 50000
            workers = max(2, min(8, (os.cpu_count() or 4)))
            candidates = []
            try:
                with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as pool:
                    futures = []
                    for i in range(0, total, chunk_size):
                        chunk = lines[i:i+chunk_size]
                        futures.append(pool.submit(CometImportThread._static_process_chunk, chunk, ex, ey, ez, cx, cy, rad, self.obs_time.timestamp()))
                    done = 0
                    for fut in concurrent.futures.as_completed(futures):
                        try:
                            candidates.extend(fut.result())
                        except Exception:
                            pass
                        done += 1
                        self.progress_signal.emit(f"正在导入: {min(done*chunk_size, total)}/{total}")
            except Exception:
                candidates = CometImportThread._static_process_chunk(lines, ex, ey, ez, cx, cy, rad, self.obs_time.timestamp())
            if candidates:
                ras = [c['ra'] for c in candidates]
                decs = [c['dec'] for c in candidates]
                sc = SkyCoord(ras * u.deg, decs * u.deg)
                xs, ys = self.wcs.world_to_pixel(sc)
                res_markers = []
                for i in range(len(ras)):
                    label = candidates[i]['label']
                    res_markers.append({'x': float(xs[i]), 'y': float(ys[i]), 'type': 'asteroid', 'label': label})
                self.result_signal.emit(res_markers)
            else:
                self.result_signal.emit([])
        except Exception as e:
            self.error_signal.emit(str(e))
class ImageGraphicsView(QGraphicsView):
    """自定义的图像视图，支持缩放、平移和点击记录"""
    clicked = pyqtSignal(QPointF)  # 点击信号
    mouse_moved = pyqtSignal(QPointF)  # 鼠标移动信号
    wheel_scrolled = pyqtSignal(int)  # 滚轮信号，传递方向
    double_clicked = pyqtSignal(QPointF)  # 双击信号

    def __init__(self, parent=None):
        super().__init__(parent)
        self.pixel_scale = None
        self.wcs = None
        self.distance_label = QLabel("距离: --")
        self.distance_label.setStyleSheet("QLabel { color: yellow; font-weight: bold; }")
        self.distance_label.hide()
        
    def set_wcs_info(self, wcs, pixel_scale):
        """设置WCS和像素比例信息"""
        self.wcs = wcs
        self.pixel_scale = pixel_scale
        
            
    def reset_measurement(self):
        """重置测量状态"""
        if self.measure_line:
            self.scene().removeItem(self.measure_line)
            self.start_point = None
        self.end_point = None
        self.distance_label.setText("距离: --")
        
    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if self.measuring and event.button() == Qt.LeftButton:
            self.start_point = self.mapToScene(event.pos())
            self.measure_line = QGraphicsLineItem(QLineF(self.start_point, self.start_point))
            self.measure_line.setPen(QPen(Qt.yellow, 2, Qt.SolidLine))
            self.scene().addItem(self.measure_line)
        else:
            super().mousePressEvent(event)
            
    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        if self.measuring and self.start_point and self.measure_line:
            current_point = self.mapToScene(event.pos())
            self.measure_line.setLine(QLineF(self.start_point, current_point))
            self.calculate_distance(current_point)
        else:
            super().mouseMoveEvent(event)
            
    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if self.measuring and event.button() == Qt.LeftButton and self.start_point:
            self.end_point = self.mapToScene(event.pos())
            self.calculate_distance(self.end_point)
        else:
            super().mouseReleaseEvent(event)
            
            self.distance_label.setText(f"距离: {pixel_distance:.2f} 像素")
    """自定义的图像视图，支持缩放、平移和点击记录"""
    clicked = pyqtSignal(QPointF)  # 点击信号
    mouse_moved = pyqtSignal(QPointF)  # 鼠标移动信号
    wheel_scrolled = pyqtSignal(int)  # 滚轮信号，传递方向
    double_clicked = pyqtSignal(QPointF)  # 双击信号

    def get_main_window(self):
        """遍历父对象查找主窗口实例"""
        parent = self.parent()
        while parent:
            if isinstance(parent, QMainWindow):
                return parent
            parent = parent.parent()
        return None
        
    def is_animation_playing(self):
        """检查是否正在播放动画"""
        main_window = self.get_main_window()
        if main_window and hasattr(main_window, 'is_playing'):
            return main_window.is_playing
        return False

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setRenderHint(QPainter.Antialiasing, True)
        self.setRenderHint(QPainter.SmoothPixmapTransform, True)
        # 将拖动模式改为NoDrag，避免小手光标，但会在鼠标事件中自己实现拖动
        self.setDragMode(QGraphicsView.NoDrag)
        self.setCursor(Qt.ArrowCursor)
        self.viewport().setCursor(Qt.ArrowCursor)  # 强制设置箭头光标
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)  # 以鼠标为中心进行变换
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.setBackgroundBrush(QColor(0, 0, 0))  # 黑色背景
        self.setFrameShape(QFrame.NoFrame)  # 无边框
        
        # 初始化settings属性
        self.settings = configparser.ConfigParser()
        self.settings.read('config.ini')
        
        self.zoom_factor = 1.0
        self.markers = []  # 存储标记点
        self.filename = ""  # 当前显示的文件名
        self.auxiliary_lines = []  # 存储辅助线
        self.drawing_auxiliary_line = False  # 是否正在绘制辅助线
        self.auxiliary_line_start = None  # 辅助线起点
        self.auxiliary_line_temp = None  # 临时辅助线（绘制过程中）
        self.color_filter = None  # 颜色滤镜
        
        # 用于实现拖动功能
        self.is_panning = False
        self.last_mouse_position = None
        self._click_press_pos = None
        self._has_moved = False
        
        # 滚轮模式控制 (True表示滚轮放大，False表示滚轮切换)
        self.wheel_zoom_mode = True
        
        # 创建场景
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        
        # 当前图像项
        self.pixmap_item = None
        self.original_pixmap = None
        
        # 文件名文本项
        self.filename_item = None
        self.asteroid_overlay_item = None
        self.asteroid_overlay_pixmap = None
        
        # 延迟一小段时间后尝试恢复辅助线
        QTimer.singleShot(500, self.restore_auxiliary_lines)
        
        # 放大镜相关
        self.magnifier_enabled = False
        self.magnifier_size = QSize(500, 500)  # 修改为500x500像素
        self.magnifier_scale = 4.0
        self.magnifier_widget = QLabel()
        self.magnifier_widget.setFixedSize(self.magnifier_size)
        self.magnifier_widget.setVisible(False)
        # 样式：1px 实线 #333 边框，80% 透明白背景
        self.magnifier_widget.setStyleSheet("QLabel { border: 1px solid #333333; background-color: rgba(255,255,255,204); }")
        self.magnifier_widget.setAttribute(Qt.WA_TransparentForMouseEvents, True)  # 不拦截鼠标
        self.magnifier_widget.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.magnifier_widget.setAttribute(Qt.WA_TranslucentBackground)
        self._update_magnifier_position()
        
        # 开启鼠标跟踪以便不按住也能触发 mouseMoveEvent
        self.setMouseTracking(True)
        self.viewport().setMouseTracking(True)

    def wheelEvent(self, event):
        """处理鼠标滚轮事件，实现图片切换功能"""
        if (event.modifiers() & Qt.ControlModifier) or self.wheel_zoom_mode:  # Ctrl+滚轮或滚轮放大模式下进行缩放
            step = 0.2  # 将步进值从1.0改为0.2实现更平滑缩放
            if event.angleDelta().y() < 0:  # 向下滚动，缩小
                step = -step
            
            self.zoom_factor += step
            self.zoom_factor = max(0.3, min(10.0, self.zoom_factor))  # 保持10倍上限
            
            # 将缩放因子转换为滑块值并更新
            slider_value = int(self.zoom_factor * 10)
            # 获取主窗口引用并更新缩放
            main_window = self.get_main_window()
            if main_window:
                main_window.update_zoom(slider_value)
                main_window.update_zoom_label(self.zoom_factor)
            
            self.setTransform(QTransform().scale(self.zoom_factor, self.zoom_factor))
            
            # 通知主窗口更新缩放标签
            parent = self.parent()
            while parent and not isinstance(parent, QMainWindow):
                parent = parent.parent()
            if parent:
                try:
                    parent.update_zoom_label(self.zoom_factor)
                except AttributeError:
                    pass
            event.accept()  # 添加事件接受确保正确处理
            return
        
        # 不按Ctrl时，滚轮用于切换图片
        direction = 1 if event.angleDelta().y() < 0 else -1  # 向下滚动为下一张，向上滚动为上一张
        self.wheel_scrolled.emit(direction)

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.LeftButton:
            if self.drawing_auxiliary_line:
                self.auxiliary_line_start = self.mapToScene(event.pos())
                self.auxiliary_line_temp = self.scene.addLine(
                    self.auxiliary_line_start.x(), self.auxiliary_line_start.y(),
                    self.auxiliary_line_start.x(), self.auxiliary_line_start.y(),
                    QPen(QColor('yellow'), 0.5, Qt.SolidLine)
                )
                return
            else:
                # 左键按住拖动图片
                self.last_mouse_position = event.pos()
                self.is_panning = True
                self.viewport().setCursor(Qt.ArrowCursor)
                self._click_press_pos = event.pos()
                self._has_moved = False
                return
        elif event.button() == Qt.RightButton:
            # 发射右键点击信号
            scene_pos = self.mapToScene(event.pos())
            try:
                # 只在图像区域内发射信号
                if hasattr(self, 'pixmap_item') and self.pixmap_item and self.pixmap_item.contains(scene_pos):
                    self.clicked.emit(scene_pos)
            except Exception as e:
                print(f"处理右键点击时出错: {e}")
            # 不阻止上下文菜单显示，让contextMenuEvent处理
            return
        elif event.button() == Qt.MiddleButton:
            self.last_mouse_position = event.pos()
            self.is_panning = True
            self.viewport().setCursor(Qt.ArrowCursor)
            self._click_press_pos = None
            self._has_moved = False
            return
        # 其他按钮交给基类处理
        super().mousePressEvent(event)

    def keyPressEvent(self, event):
        """键盘按下事件"""
        if event.key() in (Qt.Key_Left, Qt.Key_Right) and not (event.modifiers() & Qt.ControlModifier):
            main_window = self.get_main_window()
            if main_window and hasattr(main_window, 'animation_mode') and main_window.animation_mode:
                if event.key() == Qt.Key_Left:
                    main_window.change_animation(-1)
                else:
                    main_window.change_animation(1)
                return
        if event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_1:
            # 切换滚轮模式
            self.wheel_zoom_mode = not self.wheel_zoom_mode
            # 显示状态提示
            mode = "滚轮放大" if self.wheel_zoom_mode else "滚轮切换"
            QMessageBox.information(self, "模式切换", f"当前滚轮模式: {mode}", QMessageBox.Ok)
        elif event.key() == Qt.Key_F3 and not (event.modifiers() & Qt.ControlModifier):
            # F3 开关放大镜
            self.magnifier_enabled = not self.magnifier_enabled
            self.magnifier_widget.setVisible(self.magnifier_enabled)
            self._update_magnifier_position()
            # 立即刷新一次内容
            if self.magnifier_enabled:
                try:
                    self._update_magnifier_content(self.mapToScene(self.mapFromGlobal(QCursor.pos())))
                except Exception:
                    pass
        elif event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_F:
            # CTRL+F 显示FITS头文件信息
            main_window = self.get_main_window()
            if main_window:
                main_window.show_fits_header()
        else:
            super().keyPressEvent(event)
            
    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        scene_pos = self.mapToScene(event.pos())
        self.mouse_moved.emit(scene_pos)
        
        # 获取鼠标位置的坐标信息并更新状态栏
        x, y = int(scene_pos.x()), int(scene_pos.y())
        
        # 获取主窗口引用
        parent = self.parent()
        while parent and not isinstance(parent, QMainWindow):
            parent = parent.parent()
            
        if parent and hasattr(parent, 'coord_label'):
            # 基本像素坐标信息
            coord_text = f"X: {x}, Y: {y}"
            
            # 如果有WCS信息，添加天文坐标
            if hasattr(parent, 'image_processor') and parent.image_processor and hasattr(parent.image_processor, 'wcs') and parent.image_processor.wcs:
                try:
                    # 使用WCS将像素坐标转换为天文坐标
                    sky_coords = parent.image_processor.wcs.pixel_to_world(x, y)
                    
                    # 获取原始坐标值
                    ra_hour = sky_coords.ra.hour
                    dec_deg = sky_coords.dec.deg
                    
                    # 格式化赤经坐标（时分秒）
                    ra_h = int(ra_hour)
                    ra_m = int((ra_hour - ra_h) * 60)
                    ra_s = ((ra_hour - ra_h) * 60 - ra_m) * 60
                    
                    # 格式化赤纬坐标（度分秒）
                    dec_sign = '+' if dec_deg >= 0 else '-'
                    dec_d = int(abs(dec_deg))
                    dec_m = int((abs(dec_deg) - dec_d) * 60)
                    dec_s = ((abs(dec_deg) - dec_d) * 60 - dec_m) * 60
                    
                    # 构建格式化的坐标字符串
                    ra_s_int = int(ra_s)
                    ra_s_frac = ra_s - ra_s_int
                    ra_s_str = f"{ra_s_int:02d}.{int(ra_s_frac * 100):02d}"
                    
                    dec_s_int = int(dec_s)
                    dec_s_frac = dec_s - dec_s_int
                    dec_s_str = f"{dec_s_int:02d}.{int(dec_s_frac * 100):02d}"
                    
                    # 最终坐标字符串
                    ra_str = f"{ra_h:02d} {ra_m:02d} {ra_s_str}"
                    dec_str = f"{dec_sign}{dec_d:02d} {dec_m:02d} {dec_s_str}"
                    
                    coord_text = f"RA: {ra_str} DEC: {dec_str} | {coord_text}"
                except Exception as e:
                    # 坐标转换失败，尝试自动动画模式下的WCS
                    try:
                        if hasattr(parent, 'animation_mode') and parent.animation_mode and hasattr(parent, 'animation_sets') and parent.animation_sets:
                            idx = parent.current_animation_index % len(parent.animation_sets)
                            item = parent.animation_sets[idx]
                            if item.get('old_wcs') is not None:
                                sky_coords = item['old_wcs'].pixel_to_world(x, y)
                                ra_deg = sky_coords.ra.deg
                                dec_deg = sky_coords.dec.deg
                                ra_hour = sky_coords.ra.hour
                                ra_h = int(ra_hour)
                                ra_m = int((ra_hour - ra_h) * 60)
                                ra_s = ((ra_hour - ra_h) * 60 - ra_m) * 60
                                dec_sign = '+' if dec_deg >= 0 else '-'
                                dec_d = int(abs(dec_deg))
                                dec_m = int((abs(dec_deg) - dec_d) * 60)
                                dec_s = ((abs(dec_deg) - dec_d) * 60 - dec_m) * 60
                                ra_s_int = int(ra_s)
                                ra_s_frac = ra_s - ra_s_int
                                ra_s_str = f"{ra_s_int:02d}.{int(ra_s_frac * 100):02d}"
                                dec_s_int = int(dec_s)
                                dec_s_frac = dec_s - dec_s_int
                                dec_s_str = f"{dec_s_int:02d}.{int(dec_s_frac * 100):02d}"
                                ra_str = f"{ra_h:02d} {ra_m:02d} {ra_s_str}"
                                dec_str = f"{dec_sign}{dec_d:02d} {dec_m:02d} {dec_s_str}"
                                coord_text = f"RA: {ra_str} DEC: {dec_str} | {coord_text}"
                    except Exception:
                        pass
            
            # 更新坐标标签
            parent.coord_label.setText(coord_text)
        
        # 如果正在绘制辅助线，更新临时线的终点
        if self.drawing_auxiliary_line and self.auxiliary_line_start and self.auxiliary_line_temp:
            self.auxiliary_line_temp.setLine(
                self.auxiliary_line_start.x(), self.auxiliary_line_start.y(),
                scene_pos.x(), scene_pos.y()
            )
        # 实现拖动功能，允许在任何状态下拖动图片
        elif self.is_panning and self.last_mouse_position:
            # 计算移动偏移
            delta = event.pos() - self.last_mouse_position
            self.last_mouse_position = event.pos()
            
            # 移动视图
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
            if not self._has_moved and (abs(delta.x()) > 3 or abs(delta.y()) > 3):
                self._has_moved = True
        
        # 放大镜内容更新
        if getattr(self, 'magnifier_enabled', False):
            try:
                self._update_magnifier_content(scene_pos)
            except Exception:
                pass
        
        super().mouseMoveEvent(event)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        try:
            self._update_magnifier_position()
        except Exception:
            pass

    def _update_magnifier_position(self):
        try:
            if not hasattr(self, 'magnifier_widget') or self.magnifier_widget is None:
                return
            margin = 10
            # 获取主窗口
            main_window = self.window()
            if main_window:
                # 获取主窗口在屏幕上的位置
                main_window_pos = main_window.pos()
                # 计算相对于屏幕的右下角位置
                x = main_window_pos.x() + main_window.width() - self.magnifier_size.width() - margin
                y = main_window_pos.y() + main_window.height() - self.magnifier_size.height() - margin
                # 设置放大镜位置
                self.magnifier_widget.move(x, y)
        except Exception:
            pass

    def _update_magnifier_content(self, scene_pos):
        try:
            if not getattr(self, 'magnifier_enabled', False):
                return
            if not hasattr(self, 'original_pixmap') or self.original_pixmap is None or self.original_pixmap.isNull():
                return
            src_w = max(1, int(self.magnifier_size.width() / self.magnifier_scale))
            src_h = max(1, int(self.magnifier_size.height() / self.magnifier_scale))
            half_w = src_w // 2
            half_h = src_h // 2
            cx = int(scene_pos.x())
            cy = int(scene_pos.y())
            img_rect = self.original_pixmap.rect()
            src_rect = QRect(cx - half_w, cy - half_h, src_w, src_h).intersected(img_rect)
            if src_rect.isEmpty():
                return
            cropped = self.original_pixmap.copy(src_rect)
            scaled = cropped.scaled(self.magnifier_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            self.magnifier_widget.setPixmap(scaled)
        except Exception:
            pass

    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton:
            if self.drawing_auxiliary_line and self.auxiliary_line_start:
                scene_pos = self.mapToScene(event.pos())
                # 删除临时线
                if self.auxiliary_line_temp:
                    self.scene.removeItem(self.auxiliary_line_temp)
                    self.auxiliary_line_temp = None
                
                # 如果起点和终点不同，添加正式的辅助线
                if (abs(scene_pos.x() - self.auxiliary_line_start.x()) > 5 or 
                    abs(scene_pos.y() - self.auxiliary_line_start.y()) > 5):
                    self.add_auxiliary_line(self.auxiliary_line_start, scene_pos)
                
                self.auxiliary_line_start = None
            else:
                # 结束左键拖动
                self.is_panning = False
                self.last_mouse_position = None
                self._click_press_pos = None
                self._has_moved = False
        elif event.button() == Qt.MiddleButton:
            self.is_panning = False
            self.last_mouse_position = None
            self._has_moved = False
        
        super().mouseReleaseEvent(event)
        
    def contextMenuEvent(self, event):
        """右键菜单事件"""
        # 确保有图像显示
        if not hasattr(self, 'pixmap_item') or not self.pixmap_item:
            return
            
        # 获取鼠标位置
        scene_pos = self.mapToScene(event.pos())
        
        # 检查点击位置是否在图像上
        if not self.pixmap_item.contains(scene_pos):
            return
            
        # 创建右键菜单
        menu = QMenu(self)
        
        # 添加菜单项 - 复制坐标
        copy_coord_action = menu.addAction("复制坐标")
        
        # 检查是否点击在小行星标记附近，如果是，添加取消标记菜单项
        remove_marker_action = None
        for marker in self.markers:
            # 计算距离
            dx = marker['pos'].x() - scene_pos.x()
            dy = marker['pos'].y() - scene_pos.y()
            distance = (dx*dx + dy*dy) ** 0.5
            
            # 如果点击位置在标记点附近，并且是小行星标记
            if distance < 15 and marker.get('type') == 'asteroid':
                remove_marker_action = menu.addAction("取消小行星标记")
                break
        
        # 添加菜单项 - 天文数据库查询
        # 获取当前坐标点的天文坐标（如果有WCS信息）
        x, y = int(scene_pos.x()), int(scene_pos.y())
        ra_deg, dec_deg = None, None
        main_window = self.get_main_window()
        if main_window and hasattr(main_window, 'image_processor') and main_window.image_processor and hasattr(main_window.image_processor, 'wcs') and main_window.image_processor.wcs:
            try:
                sky_coords = main_window.image_processor.wcs.pixel_to_world(x, y)
                ra_deg = sky_coords.ra.deg
                dec_deg = sky_coords.dec.deg
            except Exception as e:
                print(f"WCS转换出错: {e}")
        
        # 只有当有天文坐标时才添加天文数据库查询菜单
        if ra_deg is not None and dec_deg is not None:
            astro_db_menu = menu.addMenu("天文数据库查询")
            # 图像数据库
            image_db_menu = astro_db_menu.addMenu("图像数据库")
            ps1_action = image_db_menu.addAction("PS1")
            dls_action = image_db_menu.addAction("DLS")            
            dss_action = image_db_menu.addAction("DSS")
            sdss_action = image_db_menu.addAction("SDSS")
            aladin_action = image_db_menu.addAction("Aladin")
            skyview_action = image_db_menu.addAction("SkyView")
            
            # 目录数据库
            catalog_db_menu = astro_db_menu.addMenu("目录数据库")
            vizier_action = catalog_db_menu.addAction("Vizier")
            simbad_action = catalog_db_menu.addAction("SIMBAD")
            
            # 已知天体数据库
            special_db_menu = astro_db_menu.addMenu("已知查询")
            asteroid_action = special_db_menu.addAction("小行星")
            vsx_action = special_db_menu.addAction("VSX变星")
            tns_action = special_db_menu.addAction("TNS超新星")
        
        # 添加菜单项 - 保存图像
        save_action = menu.addAction("保存图像")
        
        # 显示菜单并获取选择的操作
        action = menu.exec_(event.globalPos())
        
        # 处理菜单操作
        try:
            # 获取主窗口引用
            main_window = self.get_main_window()
            if not main_window:
                return
                
            if action == copy_coord_action:
                # 复制坐标到剪贴板
                x, y = int(scene_pos.x()), int(scene_pos.y())
                
                # 如果有WCS信息，复制HH MM SS格式的天文坐标
                if hasattr(main_window, 'image_processor') and main_window.image_processor and hasattr(main_window.image_processor, 'wcs') and main_window.image_processor.wcs:
                    try:
                        # 使用WCS将像素坐标转换为天文坐标
                        sky_coords = main_window.image_processor.wcs.pixel_to_world(x, y)
                        
                        # 格式化赤经坐标（时分秒）
                        ra_h = int(sky_coords.ra.hour)
                        ra_m = int((sky_coords.ra.hour - ra_h) * 60)
                        ra_s = ((sky_coords.ra.hour - ra_h) * 60 - ra_m) * 60
                        
                        # 格式化赤纬坐标（度分秒）
                        dec_sign = '+' if sky_coords.dec.deg >= 0 else '-'
                        dec_d = int(abs(sky_coords.dec.deg))
                        dec_m = int((abs(sky_coords.dec.deg) - dec_d) * 60)
                        dec_s = ((abs(sky_coords.dec.deg) - dec_d) * 60 - dec_m) * 60
                        
                        # 构建格式化的坐标字符串
                        ra_str = f"{ra_h:02d} {ra_m:02d} {ra_s:05.2f}"
                        dec_str = f"{dec_sign}{dec_d:02d} {dec_m:02d} {dec_s:05.2f}"
                        
                        coord_text = f"{ra_str} {dec_str}"
                    except Exception as e:
                        print(f"WCS转换出错: {e}")
                        QMessageBox.warning(self, "错误", "无法获取天文坐标")
                        return
                else:
                    QMessageBox.warning(self, "错误", "没有WCS信息，无法获取天文坐标")
                    return
                
                # 复制到剪贴板
                from PyQt5.QtWidgets import QApplication
                QApplication.clipboard().setText(coord_text)
                if hasattr(main_window, 'statusBar'):
                    main_window.statusBar().showMessage(f"已复制坐标: {coord_text}")
            
            # 处理取消小行星标记
            elif action == remove_marker_action:
                # 移除标记
                success, removed_marker = self.remove_marker(scene_pos)
                if success and main_window:
                    # 添加到历史记录
                    if hasattr(main_window, 'history_logger'):
                        history_record = main_window.history_logger.add_record(
                            "取消小行星标记",
                            f"取消小行星标记: {removed_marker.get('label', '')} 在位置 X={removed_marker['pos'].x():.2f}, Y={removed_marker['pos'].y():.2f}"
                        )
                        if hasattr(main_window, 'update_history_display'):
                            main_window.update_history_display(history_record)
                    
                    # 更新状态栏
                    if hasattr(main_window, 'statusBar'):
                        main_window.statusBar().showMessage(f"已取消小行星标记: {removed_marker.get('label', '')}")
            
            # 处理天文数据库查询菜单项
            elif ra_deg is not None and dec_deg is not None:
                # 图像数据库
                # PS1查询
                if action == ps1_action:
                    self.open_astro_database_url("PS1", ra_deg, dec_deg)
                # DLS查询
                elif action == dls_action:
                    self.open_astro_database_url("DLS", ra_deg, dec_deg)
                # DSS查询
                elif action == dss_action:
                    self.open_astro_database_url("DSS", ra_deg, dec_deg)
                # SDSS查询
                elif action == sdss_action:
                    self.open_astro_database_url("SDSS", ra_deg, dec_deg)
                # Aladin查询
                elif action == aladin_action:
                    self.open_astro_database_url("Aladin", ra_deg, dec_deg)
                # SkyView查询
                elif action == skyview_action:
                    self.open_astro_database_url("SkyView", ra_deg, dec_deg)
                
                # 目录数据库
                # Vizier查询
                elif action == vizier_action:
                    self.open_astro_database_url("Vizier", ra_deg, dec_deg)
                # SIMBAD查询
                elif action == simbad_action:
                    self.open_astro_database_url("SIMBAD", ra_deg, dec_deg)
                
                # 已知天体数据库
                # 小行星查询
                elif action == asteroid_action:
                    self.open_astro_database_url("asteroid", ra_deg, dec_deg)
                # VSX变星查询
                elif action == vsx_action:
                    self.open_astro_database_url("VSX", ra_deg, dec_deg)
                # TNS查询
                elif action == tns_action:
                    self.open_astro_database_url("TNS", ra_deg, dec_deg)
                    
            # 处理保存图像操作
            elif action == save_action:
                if hasattr(main_window, 'save_current_image'):
                    main_window.save_current_image()
        except Exception as e:
            print(f"处理右键菜单操作时出错: {e}")
            import traceback
            traceback.print_exc()
    
    def get_main_window(self):
        """获取主窗口引用"""
        parent = self.parent()
        while parent and not isinstance(parent, QMainWindow):
            parent = parent.parent()
        return parent

    def mouseDoubleClickEvent(self, event):
        """处理鼠标双击事件"""
        if event.button() == Qt.LeftButton:
            # 双击左键记录坐标
            scene_pos = self.mapToScene(event.pos())
            self.double_clicked.emit(scene_pos)  # 发送双击信号
        else:
            super().mouseDoubleClickEvent(event)

    
    def set_image(self, pixmap, filename=""):
        """设置图像"""
        try:
            # 在清除场景前，需要清空辅助线引用
            self.auxiliary_lines.clear()  # 先清空辅助线列表，因为scene.clear()会删除所有项目
            
            # 清除临时标记
            if hasattr(self, 'temp_markers'):
                self.temp_markers = []
                
            # 清除标记点
            self.markers.clear()
            
            # 清空场景
            if self.scene:
                self.scene.clear()
                self.asteroid_overlay_item = None
            
            self.filename = filename
            
            if pixmap and not pixmap.isNull():
                # 强制创建新的pixmap副本，避免缓存问题
                self.original_pixmap = QPixmap(pixmap)
                
                # 应用颜色滤镜
                display_pixmap = None
                try:
                    if self.color_filter:
                        display_pixmap = self.apply_color_filter(self.original_pixmap)
                    else:
                        display_pixmap = QPixmap(self.original_pixmap)  # 创建副本避免缓存问题
                except Exception as e:
                    print(f"应用颜色滤镜出错: {e}")
                    display_pixmap = QPixmap(self.original_pixmap)  # 出错时使用原始图像的副本
                    
                # 添加图像到场景
                if self.scene:
                    self.pixmap_item = self.scene.addPixmap(display_pixmap)
                    self.scene.setSceneRect(QRectF(pixmap.rect()))
                    
                    # 保持当前缩放因子，避免每次重置为1.0
                    if self.zoom_factor == 0:  # 初始状态
                        self.zoom_factor = 1.0
                        self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
                    
                    # 添加文件名显示
                    if filename:
                        try:
                            # 创建文本项显示文件名
                            self.filename_item = self.scene.addText(filename)
                            self.filename_item.setDefaultTextColor(Qt.white)
                            self.filename_item.setPos(10, 10)  # 左上角位置
                            
                            # 创建半透明背景
                            rect = self.filename_item.boundingRect()
                            bg_rect = self.scene.addRect(rect.adjusted(-5, -5, 5, 5), 
                                                      QPen(Qt.transparent), 
                                                      QBrush(QColor(0, 0, 0, 128)))
                            bg_rect.setPos(10, 10)
                            bg_rect.setZValue(-1)  # 确保背景在文本后面
                        except Exception as e:
                            print(f"添加文件名显示出错: {e}")
                
                # 恢复辅助线
                try:
                    self.restore_auxiliary_lines()
                except Exception as e:
                    print(f"恢复辅助线出错: {e}")
        except Exception as e:
            print(f"设置图像出错: {e}")
            import traceback
            traceback.print_exc()  # 打印详细错误信息

    def set_asteroid_overlay_pixmap(self, overlay_pixmap):
        if not self.scene or not overlay_pixmap:
            return
        try:
            if self.asteroid_overlay_item and self.asteroid_overlay_item.scene() == self.scene:
                try:
                    self.scene.removeItem(self.asteroid_overlay_item)
                except RuntimeError:
                    pass
            self.asteroid_overlay_item = self.scene.addPixmap(overlay_pixmap)
            self.asteroid_overlay_item.setZValue(25)
        except Exception:
            pass

    def clear_asteroid_overlay(self):
        try:
            if self.asteroid_overlay_item and self.asteroid_overlay_item.scene() == self.scene:
                self.scene.removeItem(self.asteroid_overlay_item)
            self.asteroid_overlay_item = None
            self.asteroid_overlay_pixmap = None
        except Exception:
            self.asteroid_overlay_item = None
            self.asteroid_overlay_pixmap = None
    def open_astro_database_url(self, database_type, ra_deg, dec_deg):
        """打开天文数据库网站
        
        Args:
            database_type: 数据库类型，如'PS1', 'DLS', 'DSS', 'asteroid', 'VSX', 'Vizier', 'SIMBAD', 'TNS', 'SDSS', 'Aladin', 'SkyView'
            ra_deg: 赤经（度）
            dec_deg: 赤纬（度）
        """
        from PyQt5.QtCore import QUrl
        from PyQt5.QtGui import QDesktopServices
        
        # 获取主窗口引用，用于状态栏显示
        main_window = self.get_main_window()
        
        # 根据数据库类型构建URL
        url = None
        search_radius = 1 # 默认搜索半径（度）
        
        if database_type == "PS1":
            # PanSTARRS DR1 查询
            url = f"https://ps1images.stsci.edu/cgi-bin/ps1cutouts?pos={ra_deg}+{dec_deg}&filter=color&filter=g&filter=r&filter=i&filter=z&filter=y&filetypes=stack&auxiliary=data&size=240&output_size=240&verbose=0&autoscale=99.500000&catlist="
            if main_window:
                main_window.statusBar().showMessage(f"正在查询PS1数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")

        elif database_type == "DLS":
            # DLS查询
            url = f"https://www.legacysurvey.org/viewer/jpeg-cutout/?ra={ra_deg}&dec={dec_deg}&layer=ls-dr9&pixscale=0.5&bands=grz"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询DLS数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
        
        elif database_type == "DSS":
            # DSS查询
            url = f"https://archive.stsci.edu/cgi-bin/dss_search?v=poss2ukstu_red&r={ra_deg}&d={dec_deg}&e=J2000&h=5.0&w=5.0&f=gif&c=none&fov=NONE&v3="
            if main_window:
                main_window.statusBar().showMessage(f"正在查询DSS数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")

        elif database_type == "asteroid":
            # asteroid查询
            from datetime import datetime
            # 使用图像头文件中的观测时间
            observation_date = None
            if main_window and hasattr(main_window, 'image_processor') and main_window.image_processor and hasattr(main_window.image_processor, 'header'):
                # 尝试从头文件中获取观测时间
                try:
                    if 'DATE-OBS' in main_window.image_processor.header:
                        observation_date = datetime.fromisoformat(main_window.image_processor.header['DATE-OBS'].replace('Z', '+00:00'))
                    elif 'DATE' in main_window.image_processor.header:
                        observation_date = datetime.fromisoformat(main_window.image_processor.header['DATE'].replace('Z', '+00:00'))
                except (ValueError, TypeError) as e:
                    print(f"解析观测时间出错: {e}")
            
            # 如果没有找到观测时间，使用当前时间
            if not observation_date:
                observation_date = datetime.now()
                print(f"未找到观测时间，使用当前时间: {observation_date}")
            
            # 格式化日期
            year = observation_date.year
            month = observation_date.month
            # 计算day的小数部分（小时/24 + 分钟/1440 + 秒/86400），然后四舍五入保留两位小数
            day = round(observation_date.day + observation_date.hour/24 + observation_date.minute/1440 + observation_date.second/86400, 2)
            
            # 将赤经(RA)从度转换为时分秒
            ra_h = int(ra_deg / 15)
            ra_m = int((ra_deg / 15 - ra_h) * 60)
            ra_s = ((ra_deg / 15 - ra_h) * 60 - ra_m) * 60
            
            # 处理赤纬(DEC)的度分秒转换
            dec_sign = '+' if dec_deg >= 0 else '-'
            dec_abs = abs(dec_deg)
            dec_d = int(dec_abs)
            dec_m = int((dec_abs - dec_d) * 60)
            dec_s = ((dec_abs - dec_d) * 60 - dec_m) * 60
            
            ra_str = f"{ra_h:02d}%20{ra_m:02d}%20{ra_s:05.2f}"
            dec_str = f"{dec_sign}{dec_d:02d}%20{dec_m:02d}%20{dec_s:05.2f}"
            # 使用观测站代码，如果没有设置则默认为N88
            oc = self.settings.get("Settings", "observatory_code", fallback="N88")
            url = f"http://data.minorplanetcenter.net/cgi-bin/mpcheck.cgi?year={year}&month={month}&day={day}&which=pos&ra={ra_str}&decl={dec_str}&TextArea=&limit=22&oc={oc}&radius=5&sort=d&mot=h&tmot=s&pdes=u&needed=f&ps=n&type=p"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询小行星数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")

        elif database_type == "VSX":
            # 变星查询 - 使用AAVSO VSX
            url = f"https://www.aavso.org/vsx/index.php?view=results.get&coords={ra_deg:.6f}+{dec_deg:.6f}&format=d&size=120&geom=r&unit=3&order=9"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询VSX数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
        
        elif database_type == "Vizier":
            # Vizier查询
            url = f"https://vizier.u-strasbg.fr/viz-bin/VizieR?-source=&-out.add=_r&-out.add=_RAJ,_DEJ&-sort=_r&-to=&-out.max=20&-meta.ucd=2&-meta.foot=1&-c={ra_deg:.6f}+{dec_deg:.6f}&-c.rs=1"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询Vizier数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
        
        elif database_type == "TNS":
            # TNS超新星查询
            url = f"https://www.wis-tns.org/search?&ra={ra_deg}&decl={dec_deg}&radius=1&coords_unit=arcmin"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询TNS超新星数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
                
        elif database_type == "SDSS":
            # SDSS查询
            url = f"http://skyserver.sdss.org/dr16/en/tools/chart/navi.aspx?ra={ra_deg}&dec={dec_deg}&scale=0.2&width=500&height=500&opt=G"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询SDSS数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
                
        elif database_type == "Aladin":
            # Aladin查询
            url = f"https://aladin.u-strasbg.fr/AladinLite/?target={ra_deg}%20{dec_deg}&fov=0.2&survey=P%2FDSS2%2Fcolor"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询Aladin数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
                
        elif database_type == "SkyView":
            # SkyView查询
            url = f"https://skyview.gsfc.nasa.gov/current/cgi/runquery.pl?Position={ra_deg}%2C{dec_deg}&survey=DSS2R&size=0.2&pixels=500&Return=JPEG"
            if main_window:
                main_window.statusBar().showMessage(f"正在查询SkyView数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")
                
        elif database_type == "SIMBAD":
            # SIMBAD查询
            url = f"http://simbad.u-strasbg.fr/simbad/sim-coo?Coord={ra_deg:.6f}+{dec_deg:.6f}&CooFrame=FK5&CooEpoch=2000&CooEqui=2000&CooDefinedFrames=none&Radius=1&Radius.unit=arcmin&submit=submit+query&CoordList="
            if main_window:
                main_window.statusBar().showMessage(f"正在查询SIMBAD数据库: RA={ra_deg:.6f}, DEC={dec_deg:.6f}")                
        
        # 打开URL
        if url:
            QDesktopServices.openUrl(QUrl(url))
        else:
            if main_window:
                main_window.statusBar().showMessage(f"未知的数据库类型: {database_type}")
    
    def restore_auxiliary_lines(self):
        """从当前图片设置恢复辅助线"""
        try:
            if not hasattr(self, 'current_image'):
                return
            
            settings = QSettings("CCOR", "ImageViewer")
            current_img = os.path.basename(self.current_image['filename'])
            
            # 加载当前图片的辅助线
            size = settings.beginReadArray("auxiliary_lines")
            
            # 清除当前辅助线
            for line in self.auxiliary_lines:
                if line.scene() == self.scene:
                    self.scene.removeItem(line)
            self.auxiliary_lines.clear()
            
            # 恢复辅助线
            for i in range(size):
                settings.setArrayIndex(i)
                x1 = settings.value("x1", 0.0, type=float)
                y1 = settings.value("y1", 0.0, type=float)
                x2 = settings.value("x2", 0.0, type=float)
                y2 = settings.value("y2", 0.0, type=float)
                
                # 创建辅助线
                line = self.scene.addLine(x1, y1, x2, y2, 
                    QPen(QColor('yellow'), 0.5, Qt.SolidLine))
                line.setZValue(10)
                self.auxiliary_lines.append(line)
            
            settings.endArray()
            
            # 应用全局可见性设置
            settings = QSettings("CCOR", "ImageViewer")
            lines_visible = settings.value("auxiliary_lines_visible", True, type=bool)
            self.set_auxiliary_lines_visible(lines_visible)
            
        except Exception as e:
            print(f"恢复辅助线时出错: {e}")
    
    def apply_color_filter(self, pixmap):
        """应用颜色滤镜到图像"""
        if not self.color_filter:
            return pixmap
            
        # 创建可编辑的图像
        image = pixmap.toImage()
        
        # 使用更高效的方法应用颜色滤镜
        if self.color_filter == "red":
            # 红色滤镜
            red_mask = QImage(image.size(), QImage.Format_ARGB32)
            red_mask.fill(QColor(255, 0, 0, 128))
            painter = QPainter(image)
            painter.setCompositionMode(QPainter.CompositionMode_Multiply)
            painter.drawImage(0, 0, red_mask)
            painter.end()
        elif self.color_filter == "green":
            # 绿色滤镜
            green_mask = QImage(image.size(), QImage.Format_ARGB32)
            green_mask.fill(QColor(0, 255, 0, 128))
            painter = QPainter(image)
            painter.setCompositionMode(QPainter.CompositionMode_Multiply)
            painter.drawImage(0, 0, green_mask)
            painter.end()
        elif self.color_filter == "blue":
            # 蓝色滤镜
            blue_mask = QImage(image.size(), QImage.Format_ARGB32)
            blue_mask.fill(QColor(0, 0, 255, 128))
            painter = QPainter(image)
            painter.setCompositionMode(QPainter.CompositionMode_Multiply)
            painter.drawImage(0, 0, blue_mask)
            painter.end()
                
        return QPixmap.fromImage(image)
    
    def set_color_filter(self, color=None):
        """设置颜色滤镜"""
        if color in ["red", "green", "blue", None]:
            self.color_filter = color
            # 如果有图像，重新应用滤镜
            if self.original_pixmap:
                if self.color_filter:
                    filtered_pixmap = self.apply_color_filter(self.original_pixmap)
                    if self.pixmap_item:
                        self.pixmap_item.setPixmap(filtered_pixmap)
                else:
                    if self.pixmap_item:
                        self.pixmap_item.setPixmap(self.original_pixmap)

    def add_marker(self, pos):
        """添加标记点
        pos可以是QPointF对象或包含x、y坐标的字典
        """
        if not self.pixmap_item or not self.scene:
            return None
        
        try:
            # 检查pos类型
            if isinstance(pos, dict):
                # 从字典中获取坐标
                x = pos.get('x', 0)
                y = pos.get('y', 0)
                label = pos.get('label', '')
                marker_type = pos.get('type', 'default')
                
                # 创建标记
                items = []
                
                # 根据标记类型选择不同的样式
                if marker_type == 'asteroid':
                    # 小行星标记使用绿色圆圈
                    size = 15
                    circle = self.scene.addEllipse(x-size/2, y-size/2, size, size, 
                                                QPen(Qt.green, 1.5), QBrush(Qt.transparent))
                    items.append(circle)
                    
                    # 添加标签文本
                    if label:
                        text = self.scene.addText(label)
                        text.setDefaultTextColor(Qt.green)
                        text.setPos(x + size/2 + 2, y - size/2 - 2)
                        items.append(text)
                else:
                    # 默认标记使用红色十字
                    size = 10
                    items.append(self.scene.addLine(x - size, y, x - size/2, y, Qt.red))
                    items.append(self.scene.addLine(x + size/2, y, x + size, y, Qt.red))
                    items.append(self.scene.addLine(x, y - size, x, y - size/2, Qt.red))
                    items.append(self.scene.addLine(x, y + size/2, x, y + size, Qt.red))
                
                marker_group = self.scene.createItemGroup(items)
                
                # 保存标记信息
                self.markers.append({
                    'pos': QPointF(x, y),
                    'item': marker_group,
                    'type': marker_type,
                    'label': label
                })
                return QPointF(x, y)
            else:
                # 原始的十字标记处理
                size = 10
                lines = []
                lines.append(self.scene.addLine(pos.x() - size, pos.y(), pos.x() - size/2, pos.y(), Qt.red))
                lines.append(self.scene.addLine(pos.x() + size/2, pos.y(), pos.x() + size, pos.y(), Qt.red))
                lines.append(self.scene.addLine(pos.x(), pos.y() - size, pos.x(), pos.y() - size/2, Qt.red))
                lines.append(self.scene.addLine(pos.x(), pos.y() + size/2, pos.x(), pos.y() + size, Qt.red))
                
                marker_group = self.scene.createItemGroup(lines)
                
                self.markers.append({'pos': pos, 'item': marker_group, 'type': 'default', 'label': ''})
                return pos
        except Exception as e:
            print(f"添加标记点出错: {e}")
            return None

    def clear_markers(self):
        """清除所有标记"""
        try:
            for marker in self.markers[:]:  # 使用副本遍历
                if marker['item'] and marker['item'].scene() == self.scene:
                    try:
                        self.scene.removeItem(marker['item'])
                    except RuntimeError:
                        pass  # 如果项目已被删除，忽略错误
            self.markers.clear()
        except Exception as e:
            print(f"清除标记点出错: {e}")
            self.markers.clear()  # 确保在出错时也清空标记列表
            
    def remove_marker(self, pos, distance_threshold=15):
        """移除指定位置附近的标记点
        
        Args:
            pos: QPointF对象，表示要移除标记的位置
            distance_threshold: 距离阈值，小于此距离的标记将被移除
            
        Returns:
            bool: 是否成功移除标记
        """
        try:
            # 查找最近的标记
            closest_marker = None
            closest_distance = float('inf')
            closest_index = -1
            
            for i, marker in enumerate(self.markers):
                # 计算距离
                dx = marker['pos'].x() - pos.x()
                dy = marker['pos'].y() - pos.y()
                distance = (dx*dx + dy*dy) ** 0.5
                
                if distance < closest_distance:
                    closest_distance = distance
                    closest_marker = marker
                    closest_index = i
            
            # 如果找到足够近的标记，移除它
            if closest_marker and closest_distance < distance_threshold:
                # 从场景中移除
                if closest_marker['item'] and closest_marker['item'].scene() == self.scene:
                    try:
                        self.scene.removeItem(closest_marker['item'])
                    except RuntimeError:
                        pass  # 如果项目已被删除，忽略错误
                
                # 从列表中移除
                self.markers.pop(closest_index)
                return True, closest_marker
                
            return False, None
        except Exception as e:
            print(f"移除标记点出错: {e}")
            return False, None

    def reset_view(self):
        """重置视图"""
        if self.pixmap_item:
            self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
            self.zoom_factor = 1.0

    def add_auxiliary_line(self, start_pos, end_pos):
        """添加辅助线"""
        line = self.scene.addLine(
            start_pos.x(), start_pos.y(), end_pos.x(), end_pos.y(),
            QPen(QColor('yellow'), 0.5, Qt.SolidLine)
        )
        line.setZValue(10)  # 确保辅助线在图像上方
        self.auxiliary_lines.append(line)
        self.save_auxiliary_lines()  # 保存辅助线
        return line
    
    def remove_auxiliary_line(self, index=None):
        """删除辅助线，如果index为None则删除最后一条"""
        if not self.auxiliary_lines:
            return
            
        try:
            if index is None:
                # 删除最后一条线
                line = self.auxiliary_lines.pop()
                if line.scene() == self.scene:  # 确保线条仍在场景中
                    self.scene.removeItem(line)
            elif 0 <= index < len(self.auxiliary_lines):
                # 删除指定索引的线
                line = self.auxiliary_lines.pop(index)
                if line.scene() == self.scene:  # 确保线条仍在场景中
                    self.scene.removeItem(line)
            
            # 保存辅助线状态
            self.save_auxiliary_lines()
        except Exception as e:
            print(f"删除辅助线出错: {e}")
    
    def clear_auxiliary_lines(self):
        """清除所有辅助线"""
        try:
            for line in self.auxiliary_lines:
                if line.scene() == self.scene:  # 确保线条仍在场景中
                    self.scene.removeItem(line)
            self.auxiliary_lines.clear()
            
            # 清除辅助线设置
            self.save_auxiliary_lines()
        except Exception as e:
            print(f"清除辅助线出错: {e}")
    
    def start_drawing_line(self):
        """开始绘制辅助线模式"""
        try:
            self.drawing_auxiliary_line = True
            self.setCursor(Qt.CrossCursor)  # 十字光标
            # 禁用拖动功能
            self.is_panning = False
        except Exception as e:
            print(f"开始绘制辅助线模式时出错: {e}")
    
    def stop_drawing_line(self):
        """停止绘制辅助线模式"""
        try:
            self.drawing_auxiliary_line = False
            self.setCursor(Qt.ArrowCursor)  # 恢复默认光标
            # 不再需要设置拖动模式，已经在mousePressEvent中处理
            if self.auxiliary_line_temp:
                self.scene.removeItem(self.auxiliary_line_temp)
                self.auxiliary_line_temp = None
            self.auxiliary_line_start = None
        except Exception as e:
            print(f"停止绘制辅助线模式时出错: {e}")
    
    def save_auxiliary_lines(self):
        """保存辅助线到当前图片设置"""
        try:
            if not hasattr(self, 'current_image'):
                return
            
            settings = QSettings("CCOR", "ImageViewer")
            current_img = os.path.basename(self.current_image['filename'])
            
            # 保存到当前图片分组
            settings.remove("auxiliary_lines")
            settings.beginWriteArray("auxiliary_lines")
            for i, line in enumerate(self.auxiliary_lines):
                line_coords = line.line()
                settings.setArrayIndex(i)
                settings.setValue("x1", line_coords.x1())
                settings.setValue("y1", line_coords.y1())
                settings.setValue("x2", line_coords.x2())
                settings.setValue("y2", line_coords.y2())
            settings.endArray()
            settings.sync()
        except Exception as e:
            print(f"保存辅助线出错: {e}")

    def add_temp_marker(self, pos):
        """添加临时标记点（仅为圆形，中心透明）"""
        if not self.pixmap_item or not self.scene:
            return None
            
        try:
            # 创建圆形标记
            size = 15  # 圆形大小
            circle = self.scene.addEllipse(
                pos.x() - size/2, pos.y() - size/2, 
                size, size, 
                QPen(QColor('orange'), 1.5),  # 细线条
                QBrush(Qt.transparent)  # 中心透明
            )
            circle.setZValue(20)  # 确保在最上层
            
            # 存储临时标记 (不同于普通标记，这个不会保存)
            if not hasattr(self, 'temp_markers'):
                self.temp_markers = []
            self.temp_markers.append(circle)
            
            return circle
        except Exception as e:
            print(f"添加临时标记点出错: {e}")
            return None
        
    def clear_temp_markers(self):
        """清除所有临时标记"""
        try:
            if hasattr(self, 'temp_markers'):
                for marker in self.temp_markers[:]:  # 使用列表副本进行迭代
                    if marker and marker.scene() == self.scene:
                        try:
                            self.scene.removeItem(marker)
                        except RuntimeError:
                            pass  # 忽略已删除对象的错误
                self.temp_markers = []
        except Exception as e:
            print(f"清除临时标记出错: {e}")
            if hasattr(self, 'temp_markers'):
                self.temp_markers = []  # 确保在出错时也清空列表
            
    def goto_position(self, x, y):
        """跳转到指定坐标位置"""
        if not self.pixmap_item:
            return False
            
        # 清除现有的临时标记
        self.clear_temp_markers()
        
        # 获取图像尺寸
        img_rect = self.pixmap_item.boundingRect()
        
        # 检查坐标是否在图像范围内
        if not (0 <= x <= img_rect.width() and 0 <= y <= img_rect.height()):
            return False
            
        # 添加标记
        pos = QPointF(x, y)
        self.add_temp_marker(pos)
        
        # 居中显示
        self.centerOn(pos)
        
        return True

    def are_auxiliary_lines_visible(self):
        """检查辅助线是否可见"""
        try:
            if not hasattr(self, 'auxiliary_lines') or not self.auxiliary_lines:
                return False
                
            # 检查第一条辅助线的可见性
            if self.auxiliary_lines:
                # 检查辅助线的数据结构
                if isinstance(self.auxiliary_lines[0], dict) and 'item' in self.auxiliary_lines[0]:
                    return self.auxiliary_lines[0]['item'].isVisible()
                elif hasattr(self.auxiliary_lines[0], 'isVisible'):
                    return self.auxiliary_lines[0].isVisible()
            
            return False
        except Exception as e:
            print(f"检查辅助线可见性时出错: {e}")
            return False
    
    def set_auxiliary_lines_visible(self, visible):
        """设置辅助线可见性"""
        try:
            if not hasattr(self, 'auxiliary_lines') or not self.auxiliary_lines:
                return
                
            for line in self.auxiliary_lines:
                if isinstance(line, dict) and 'item' in line and line['item']:
                    line['item'].setVisible(visible)
                elif hasattr(line, 'setVisible'):
                    line.setVisible(visible)
        except Exception as e:
            print(f"设置辅助线可见性时出错: {e}")
    

class ImageViewer(QMainWindow):
    def __init__(self, embedded_mode=False):
        super().__init__()
        
        # 设置窗口标题和大小
        self.setWindowTitle("GSUN View")
        self.resize(1200, 800)
        
        # 初始化变量
        self.images = []  # 存储图像信息的列表
        self.original_images = []  # 存储原始图像数据
        self.current_index = -1  # 当前显示的图像索引
        self.click_records = []  # 点击记录
        self.is_playing = False  # 是否正在播放
        self.current_play_speed = 10  # 当前播放速度值，默认10 (1.0倍速)
        self.play_timer = QTimer()  # 播放定时器
        self.play_timer.timeout.connect(lambda: self.change_image(1))  # 默认连接到下一张图片函数
        self.color_filter = None  # 颜色滤镜
        self.embedded_mode = embedded_mode  # 嵌入模式
        self.fullscreen_window = None  # 全屏窗口
        self.history_logger = HistoryLogger()  # 历史记录记录器
        self.current_directory = ""  # 当前图片目录
        self.is_stacked = False  # 是否处于图像叠加模式
        self.stacked_image = None  # 存储叠加后的图像
        self.animation_mode = False
        self.animation_sets = []
        self.current_animation_index = -1
        self._anim_toggle = False
        
        # 初始化配置
        self.config = configparser.ConfigParser()
        self.config.read('config.ini')
        try:
            auto_flag = self.config.get('Settings', 'auto_parse_wcs', fallback="true").strip().lower()
            self.auto_parse_wcs = (auto_flag in ("true", "1", "yes"))
        except Exception:
            self.auto_parse_wcs = False
        
        # 叠加模式相关设置
        self.brightness_value = 85  # 默认亮度值（百分比）
        self.contrast_value = 5  # 默认对比度百分位值（越小越强烈）
        self.stacking_mode = "maximum"  # 默认叠加模式
        self.gamma_value = 1.0  # 伽马值
        self.high_performance = True  # 高性能模式：默认启用以加快处理速度
        self.use_negative = False  # 是否使用反色（负片）效果
        self.asteroid_markers_hidden = False
        
        # 初始化布局
        self.layout = QVBoxLayout()
        
        # 初始化UI
        self.init_ui()
        
        # 加载配置
        self.load_config()
        
        # 记录启动历史
        history_record = self.history_logger.add_record(
            "应用启动", 
            f"GSUN View已启动"
        )
        self.update_history_display(history_record)
        
        # 设置键盘焦点策略，使窗口可以接收键盘事件
        self.setFocusPolicy(Qt.StrongFocus)
        
        # 初始化历史记录显示
        self.update_history_display()
        
    def init_ui(self):
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 创建主布局
        main_layout = QVBoxLayout(central_widget)
        main_layout.setContentsMargins(5, 5, 5, 5)  # 设置边距
        
        # 设置状态栏高度
        status_bar = self.statusBar()
        status_bar.setMaximumHeight(20)  # 减小状态栏高度
        status_bar.setSizeGripEnabled(False)  # 可选：移除右下角的大小控制柄
        
        # 添加文件名标签到状态栏中央
        self.filename_label = QLabel("尚未加载图片")
        self.filename_label.setAlignment(Qt.AlignCenter)  # 设置文本居中对齐
        self.filename_label.setStyleSheet("padding: 0 5px;") 
        status_bar.addWidget(self.filename_label, 1)  # 使用1的拉伸因子让标签占用大部分状态栏
        
        # 添加坐标显示标签到状态栏右侧
        self.coord_label = QLabel("X: 0, Y: 0")
        self.coord_label.setAlignment(Qt.AlignRight)
        self.coord_label.setStyleSheet("padding-right: 5px;")
        status_bar.addPermanentWidget(self.coord_label)  # 使用permanentWidget让它显示在右侧
        
        # 创建第一行工具栏 - 文件工具栏
        self.file_toolbar = QToolBar("文件工具栏")
        self.file_toolbar.setIconSize(QSize(32, 32))
        self.file_toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self.addToolBar(self.file_toolbar)
        
        # 创建文件按钮及其下拉菜单
        self.file_menu = QMenu(self)
        
        # 添加下拉菜单项
        # 1. 导入图像
        self.import_action = QAction(self.style().standardIcon(QStyle.SP_DialogOpenButton), "导入图像", self)
        self.import_action.triggered.connect(self.load_images)
        self.file_menu.addAction(self.import_action)
        
        # 4. 搜索图像
        self.search_image_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogContentsView), "搜索图像", self)
        self.search_image_action.triggered.connect(self.search_images)
        self.file_menu.addAction(self.search_image_action)
        
        # 5. 采集目录
        self.collect_dir_action = QAction(self.style().standardIcon(QStyle.SP_DirOpenIcon), "采集目录", self)
        self.collect_dir_action.triggered.connect(self.collect_catalog)
        self.file_menu.addAction(self.collect_dir_action)
        
        # 6. 设置
        self.settings_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogDetailedView), "设置", self)
        self.settings_action.triggered.connect(self.open_settings)  # 连接到open_settings函数
        self.file_menu.addAction(self.settings_action)
        
        # 创建文件按钮
        self.file_button_action = QAction(self.style().standardIcon(QStyle.SP_DirIcon), "文件", self)
        self.file_button_action.setMenu(self.file_menu)
        self.file_toolbar.addAction(self.file_button_action)
        
        # 创建处理按钮及其下拉菜单
        self.process_menu = QMenu(self)
        
        # 添加解析功能
        self.process_parse_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogInfoView), "解析", self)
        self.process_parse_action.triggered.connect(self.process_parse)
        self.process_parse_action.setShortcut("F6")
        self.process_menu.addAction(self.process_parse_action)
        
        # 添加伪平场功能
        self.process_flatfield_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogDetailedView), "伪平场", self)
        self.process_flatfield_action.triggered.connect(self.process_flatfield)
        self.process_menu.addAction(self.process_flatfield_action)
        
        # 添加批量伪平场功能
        self.process_batch_flatfield_action = QAction(self.style().standardIcon(QStyle.SP_DirIcon), "批量伪平场", self)
        self.process_batch_flatfield_action.triggered.connect(self.process_batch_flatfield)
        self.process_menu.addAction(self.process_batch_flatfield_action)
        
        # 添加批量对齐功能
        self.process_batch_align_action = QAction("批量对齐", self)
        self.process_batch_align_action.triggered.connect(self.batch_align_images)
        self.process_menu.addAction(self.process_batch_align_action)
        
        # 创建处理按钮
        self.process_button_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogContentsView), "处理", self)
        self.process_button_action.setMenu(self.process_menu)
        self.file_toolbar.addAction(self.process_button_action)
        
        # 创建网址按钮
        self.website_menu = QMenu(self)
        
        # 添加孙国佑个人网站链接
        sunguoyou_action = QAction("孙国佑个人网站", self)
        sunguoyou_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("http://sunguoyou.lamost.org/")))
        self.website_menu.addAction(sunguoyou_action)
        
        # 添加星明天文台链接
        xjltp_action = QAction("星明天文台", self)
        xjltp_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("http://xjltp.china-vo.org/")))
        self.website_menu.addAction(xjltp_action)
        
        # 添加PSP公众超新星链接
        psp_action = QAction("PSP公众超新星", self)
        psp_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://nadc.china-vo.org/psp/")))
        self.website_menu.addAction(psp_action)
        
        # 添加常用网站及工具链接
        tools_action = QAction("常用网站及工具", self)
        tools_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("http://sunguoyou.lamost.org/twrj.html")))
        self.website_menu.addAction(tools_action)
        
        # 创建网址按钮并添加到工具栏
        self.website_button_action = QAction(self.style().standardIcon(QStyle.SP_BrowserReload), "网址", self)
        self.website_button_action.setMenu(self.website_menu)
        self.file_toolbar.addAction(self.website_button_action)
        
        # 创建第二行工具栏 - 主工具栏
        self.toolbar = QToolBar("主工具栏")
        self.toolbar.setIconSize(QSize(32, 32))
        self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.addToolBar(self.toolbar)
        
        # 1. 导入图片按钮 - 保留在主工具栏中
        self.load_action = QAction(self.style().standardIcon(QStyle.SP_DialogOpenButton), "导入图片", self)
        self.load_action.triggered.connect(self.load_images)
        self.load_action.setShortcut("F1")
        self.toolbar.addAction(self.load_action)
        
        # 2. 图片列表按钮
        self.image_list_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogListView), "图片列表", self)
        self.image_list_action.triggered.connect(self.show_image_list_dialog)
        self.toolbar.addAction(self.image_list_action)
        
        self.toolbar.addSeparator()
        
        # 3-5. 动画播放按钮
        # 3. 向前播放
        self.play_backward_action = QAction(self.style().standardIcon(QStyle.SP_MediaSkipBackward), "向前播放", self)
        self.play_backward_action.triggered.connect(self.play_backward)
        self.toolbar.addAction(self.play_backward_action)
        
        # 4. 向后播放
        self.play_forward_action = QAction(self.style().standardIcon(QStyle.SP_MediaSkipForward), "向后播放", self)
        self.play_forward_action.triggered.connect(self.play_forward)
        self.toolbar.addAction(self.play_forward_action)
        
        # 5. 暂停
        self.pause_action = QAction(self.style().standardIcon(QStyle.SP_MediaPause), "暂停", self)
        self.pause_action.triggered.connect(self.toggle_play)
        self.pause_action.setShortcut("F2")
        self.toolbar.addAction(self.pause_action)

        self.stop_action = QAction(self.style().standardIcon(QStyle.SP_MediaStop), "停止", self)
        self.stop_action.triggered.connect(self.stop_all)
        self.toolbar.addAction(self.stop_action)
        
        self.toolbar.addSeparator()
        
        # 6-7. 前一张/后一张图片
        # 6. 前一张
        self.prev_image_action = QAction(self.style().standardIcon(QStyle.SP_MediaSeekBackward), "前一张", self)
        self.prev_image_action.triggered.connect(lambda: self.change_image(-1))
        self.prev_image_action.setShortcut("Left")
        self.toolbar.addAction(self.prev_image_action)
        
        # 7. 后一张
        self.next_image_action = QAction(self.style().standardIcon(QStyle.SP_MediaSeekForward), "后一张", self)
        self.next_image_action.triggered.connect(lambda: self.change_image(1))
        self.next_image_action.setShortcut("Right")
        self.toolbar.addAction(self.next_image_action)
        
        # 播放速度调节按钮
        self.speed_action = QAction(self.style().standardIcon(QStyle.SP_MediaVolume), "播放速度", self)
        self.speed_action.triggered.connect(self.show_speed_dialog)
        self.toolbar.addAction(self.speed_action)
        
        self.toolbar.addSeparator()
        
        # 8. Goto功能
        self.goto_action = QAction(self.style().standardIcon(QStyle.SP_DialogHelpButton), "坐标定位", self)
        self.goto_action.triggered.connect(self.show_goto_dialog)
        self.toolbar.addAction(self.goto_action)

        # 8.1 天文坐标定位功能
        self.goto_radec_action = QAction(self.style().standardIcon(QStyle.SP_ComputerIcon), "天文坐标定位", self)
        self.goto_radec_action.triggered.connect(self.show_goto_radec_dialog)
        self.toolbar.addAction(self.goto_radec_action)
        
        # 21. 报告坐标功能
        self.report_coord_action = QAction(self.style().standardIcon(QStyle.SP_DialogApplyButton), "报告定位", self)
        self.report_coord_action.triggered.connect(self.show_report_coord_dialog)
        self.report_coord_action.setShortcut("F4")
        self.toolbar.addAction(self.report_coord_action)
        
        # 报告坐标按钮已移至坐标定位按钮后面
        
        self.toolbar.addSeparator()
        
        # 9-10. 放大/缩小
        # 9. 放大
        self.zoom_in_action = QAction(self.style().standardIcon(QStyle.SP_TitleBarMaxButton), "放大", self)
        self.zoom_in_action.triggered.connect(lambda: self.update_zoom(self.zoom_slider.value() + 10))
        self.toolbar.addAction(self.zoom_in_action)
        
        # 10. 缩小
        self.zoom_out_action = QAction(self.style().standardIcon(QStyle.SP_TitleBarMinButton), "缩小", self)
        self.zoom_out_action.triggered.connect(lambda: self.update_zoom(self.zoom_slider.value() - 10))
        self.toolbar.addAction(self.zoom_out_action)
        
        self.toolbar.addSeparator()
        
        # 13-14. 辅助线功能
        # 13. 画辅助线
        self.draw_line_action = QAction(self.style().standardIcon(QStyle.SP_LineEditClearButton), "画辅助线", self)
        self.draw_line_action.triggered.connect(self.toggle_drawing_line)
        self.draw_line_action.setCheckable(True)
        self.toolbar.addAction(self.draw_line_action)
        
        # 14. 隐藏辅助线
        self.hide_lines_action = QAction(self.style().standardIcon(QStyle.SP_DialogCancelButton), "隐藏辅助线", self)
        self.hide_lines_action.triggered.connect(self.toggle_auxiliary_lines)
        self.hide_lines_action.setCheckable(True)
        self.toolbar.addAction(self.hide_lines_action)
        
        # 辅助线设置
        self.line_settings_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogDetailedView), "辅助线设置", self)
        self.line_settings_action.triggered.connect(self.show_line_settings)
        self.toolbar.addAction(self.line_settings_action)
        
        self.toolbar.addSeparator()
        
        # 15-18. 颜色滤镜 - 使用色块代替图标
        # 15. 红色滤镜
        self.red_filter_action = QAction("红色滤镜", self)
        self.red_filter_action.triggered.connect(lambda: self.apply_color_filter("red"))
        # 创建红色图标
        red_pixmap = QPixmap(24, 24)
        red_pixmap.fill(QColor(255, 0, 0))
        self.red_filter_action.setIcon(QIcon(red_pixmap))
        self.toolbar.addAction(self.red_filter_action)
        
        # 16. 绿色滤镜
        self.green_filter_action = QAction("绿色滤镜", self)
        self.green_filter_action.triggered.connect(lambda: self.apply_color_filter("green"))
        # 创建绿色图标
        green_pixmap = QPixmap(24, 24)
        green_pixmap.fill(QColor(0, 255, 0))
        self.green_filter_action.setIcon(QIcon(green_pixmap))
        self.toolbar.addAction(self.green_filter_action)
        
        # 17. 蓝色滤镜
        self.blue_filter_action = QAction("蓝色滤镜", self)
        self.blue_filter_action.triggered.connect(lambda: self.apply_color_filter("blue"))
        # 创建蓝色图标
        blue_pixmap = QPixmap(24, 24)
        blue_pixmap.fill(QColor(0, 0, 255))
        self.blue_filter_action.setIcon(QIcon(blue_pixmap))
        self.toolbar.addAction(self.blue_filter_action)
        
        # 18. 原色
        self.reset_filter_action = QAction("原色", self)
        self.reset_filter_action.triggered.connect(lambda: self.apply_color_filter(None))
        # 创建灰色图标（代表原色）
        reset_pixmap = QPixmap(24, 24)
        reset_pixmap.fill(QColor(200, 200, 200))
        self.reset_filter_action.setIcon(QIcon(reset_pixmap))
        self.toolbar.addAction(self.reset_filter_action)
        
        self.toolbar.addSeparator()
        
        # 19. 图像叠加按钮
        self.stack_action = QAction(self.style().standardIcon(QStyle.SP_ToolBarHorizontalExtensionButton), "叠加图像", self)
        self.stack_action.triggered.connect(self.toggle_stack_mode)
        self.stack_action.setCheckable(True)
        self.stack_action.setShortcut("F5")
        self.toolbar.addAction(self.stack_action)
        
        # 19.1 叠加设置按钮
        self.stack_settings_action = QAction(self.style().standardIcon(QStyle.SP_DialogApplyButton), "叠加设置", self)
        self.stack_settings_action.triggered.connect(self.adjust_stack_settings)
        self.stack_settings_action.setEnabled(False)  # 初始禁用
        self.toolbar.addAction(self.stack_settings_action)
        
        # 添加Screen Stretch按钮
        self.screen_stretch_action = QAction(self.style().standardIcon(QStyle.SP_ToolBarVerticalExtensionButton), "Screen Stretch", self)
        self.screen_stretch_action.triggered.connect(self.show_screen_stretch)
        self.toolbar.addAction(self.screen_stretch_action)
        
        # 添加自动直方图按钮
        self.auto_histogram_action = QAction(self.style().standardIcon(QStyle.SP_BrowserReload), "自动直方图", self)
        self.auto_histogram_action.triggered.connect(self.auto_histogram)
        self.auto_histogram_action.setShortcut("Alt+D")
        self.toolbar.addAction(self.auto_histogram_action)

        self.toolbar.addSeparator()

        self.auto_animation_action = QAction(self.style().standardIcon(QStyle.SP_MediaPlay), "自动动画", self)
        self.auto_animation_action.triggered.connect(self.show_auto_animation_dialog)
        self.toolbar.addAction(self.auto_animation_action)

        self.toolbar.addSeparator()

        self.import_asteroid_action = QAction("导入小行星", self)
        self.import_asteroid_action.triggered.connect(self.import_asteroids_local)
        # 自定义小行星图标
        asteroid_pixmap = QPixmap(32, 32)
        asteroid_pixmap.fill(Qt.transparent)
        ap = QPainter(asteroid_pixmap)
        try:
            ap.setRenderHint(QPainter.Antialiasing, True)
            base = QColor(140, 140, 140)
            ap.setBrush(QBrush(base))
            ap.setPen(QPen(QColor(100, 100, 100), 2))
            ap.drawEllipse(8, 8, 16, 16)
            ap.setBrush(QBrush(QColor(110, 110, 110)))
            ap.setPen(QPen(QColor(90, 90, 90), 1))
            ap.drawEllipse(12, 13, 4, 4)
            ap.drawEllipse(16, 18, 3, 3)
            ap.drawEllipse(19, 12, 5, 5)
        finally:
            ap.end()
        self.import_asteroid_action.setIcon(QIcon(asteroid_pixmap))
        self.toolbar.addAction(self.import_asteroid_action)

        self.import_comet_action = QAction("导入彗星", self)
        self.import_comet_action.triggered.connect(self.import_comets_local)
        # 自定义彗星图标
        comet_pixmap = QPixmap(32, 32)
        comet_pixmap.fill(Qt.transparent)
        cp = QPainter(comet_pixmap)
        try:
            cp.setRenderHint(QPainter.Antialiasing, True)
            grad = QLinearGradient(8, 16, 30, 16)
            grad.setColorAt(0.0, QColor(255, 255, 255, 240))
            grad.setColorAt(0.3, QColor(0, 200, 255, 160))
            grad.setColorAt(1.0, QColor(0, 200, 255, 10))
            cp.setBrush(QBrush(grad))
            cp.setPen(Qt.NoPen)
            cp.drawPolygon(
                QPointF(10, 16), QPointF(30, 10), QPointF(30, 22)
            )
            cp.setBrush(QBrush(QColor(255, 255, 255)))
            cp.setPen(QPen(QColor(220, 220, 220), 1))
            cp.drawEllipse(6, 14, 6, 6)
        finally:
            cp.end()
        self.import_comet_action.setIcon(QIcon(comet_pixmap))
        self.toolbar.addAction(self.import_comet_action)
        self.toggle_asteroid_btn = QToolButton(self)
        self.toggle_asteroid_btn.setCheckable(True)
        self.toggle_asteroid_btn.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.toggle_asteroid_btn.setAutoRaise(True)
        self.toggle_asteroid_btn.setIconSize(QSize(18, 18))
        self.toggle_asteroid_btn.setToolTip("隐藏/显示小行星标记")
        self.toggle_asteroid_btn.setChecked(self.asteroid_markers_hidden)
        self.toggle_asteroid_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
        self.toggle_asteroid_btn.toggled.connect(self.toggle_asteroid_markers_visibility)
        self.toolbar.addWidget(self.toggle_asteroid_btn)
        
        # 添加水平翻转按钮
        self.horizontal_flip_action = QAction(self.style().standardIcon(QStyle.SP_ArrowLeft), "水平翻转", self)
        self.horizontal_flip_action.triggered.connect(self.horizontal_flip)
        self.horizontal_flip_action.setShortcut("Ctrl+2")
        self.toolbar.addAction(self.horizontal_flip_action)
        
        # 添加垂直翻转按钮
        self.vertical_flip_action = QAction(self.style().standardIcon(QStyle.SP_ArrowDown), "垂直翻转", self)
        self.vertical_flip_action.triggered.connect(self.vertical_flip)
        self.vertical_flip_action.setShortcut("Ctrl+3")
        self.toolbar.addAction(self.vertical_flip_action)
        
        # 添加向左旋转90度按钮
        self.rotate_left_action = QAction(self.style().standardIcon(QStyle.SP_ArrowLeft), "向左旋转90度", self)
        self.rotate_left_action.triggered.connect(self.rotate_left)
        self.rotate_left_action.setShortcut("Ctrl+4")
        self.toolbar.addAction(self.rotate_left_action)
        
        # 添加向右旋转90度按钮
        self.rotate_right_action = QAction(self.style().standardIcon(QStyle.SP_ArrowRight), "向右旋转90度", self)
        self.rotate_right_action.triggered.connect(self.rotate_right)
        self.rotate_right_action.setShortcut("Ctrl+5")
        self.toolbar.addAction(self.rotate_right_action)
        
        self.toolbar.addSeparator()
        
        
        # 添加保存图片按钮
        self.save_image_action = QAction(self.style().standardIcon(QStyle.SP_DialogSaveButton), "保存图片", self)
        self.save_image_action.triggered.connect(self.save_current_image)
        self.save_image_action.setShortcut("Ctrl+S")
        self.toolbar.addAction(self.save_image_action)
        
        # 20. 报告展示
        self.report_action = QAction(self.style().standardIcon(QStyle.SP_FileDialogInfoView), "报告展示", self)
        self.report_action.triggered.connect(self.show_report_dialog)
        self.report_action.setShortcut("F7")
        self.toolbar.addAction(self.report_action)
        
        
        # 新增作者信息区块 ---------------------------------------------------
        self.toolbar.addSeparator()
        info_container = QWidget()
        info_layout = QVBoxLayout(info_container)
        info_layout.setContentsMargins(10, 2, 10, 2)
        info_layout.setSpacing(0)
        
        # 新增作者信息区块 ---------------------------------------------------
        self.toolbar.addSeparator()
        
        # 创建右对齐容器
        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        
        info_container = QWidget()
        info_layout = QVBoxLayout(info_container)
        info_layout.setContentsMargins(10, 2, 20, 2)  # 调整右边距为20px
        info_layout.setSpacing(0)
        
        # 作者信息
        author_label = QLabel("作者: Guoyou Sun(孙国佑) 正式版 V2.0")
        author_label.setStyleSheet("font-size: 8pt; color: black;")
        info_layout.addWidget(author_label)
        
        # 官网链接
        website_label = QLabel('<a href="http://sunguoyou.lamost.org/" style="color: blue; text-decoration: none;">官网: http://sunguoyou.lamost.org</a>')
        website_label.setOpenExternalLinks(True)
        website_label.setStyleSheet("font-size: 8pt;")
        info_layout.addWidget(website_label)
        
        # 组合spacer和信息容器
        container = QWidget()
        layout = QHBoxLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(spacer)
        layout.addWidget(info_container)
        
        self.toolbar.addWidget(container)
        # ----------------------------------------------------------------
        
        
        # 创建图像显示区域
        self.image_view = ImageGraphicsView()
        self.image_view.clicked.connect(self.on_image_clicked)
        self.image_view.double_clicked.connect(self.on_image_double_clicked)
        self.image_view.mouse_moved.connect(self.update_coordinates)
        self.image_view.wheel_scrolled.connect(self.change_image)
        main_layout.addWidget(self.image_view)
        
        # 缩放比例显示标签
        self.zoom_label = QLabel("缩放: 1.0x")
        self.zoom_label.setAlignment(Qt.AlignRight)
        self.zoom_label.setStyleSheet("color: white; background-color: rgba(0, 0, 0, 128); padding: 5px;")
        self.zoom_label.setFixedSize(100, 30)
        
        # 将缩放标签添加到图像视图上
        self.image_view.setLayout(QVBoxLayout())
        self.image_view.layout().addWidget(self.zoom_label)
        self.image_view.layout().setAlignment(Qt.AlignTop | Qt.AlignRight)
        
        # 创建隐藏的滑块用于缩放和曝光控制
        self.zoom_slider = QSlider(Qt.Horizontal)
        self.zoom_slider.setRange(3, 100)  # 0.1-10.0倍
        self.zoom_slider.setValue(10)  # 默认1.0
        self.zoom_slider.valueChanged.connect(self.update_zoom)
        self.zoom_slider.hide()
        
        # 添加缩放值标签（用于显示当前缩放比例）
        # Removed exposure slider
        
        # 创建速度滑块（隐藏）- 只在播放功能中使用
        self.speed_slider = QSlider(Qt.Horizontal)
        self.speed_slider.setRange(5, 50)  # 0.5x-5x倍速 (5 = 0.5x, 10 = 1x, 50 = 5x)
        self.speed_slider.setValue(10)  # 默认1倍速，对应100ms
        self.speed_slider.valueChanged.connect(self.update_speed)
        self.speed_slider.hide()
        
        # 初始禁用按钮
        self.disable_buttons()
        
    def keyPressEvent(self, event):
        """处理键盘按键事件"""
        if self.animation_mode and event.key() in (Qt.Key_Left, Qt.Key_Right):
            if event.key() == Qt.Key_Left:
                self.change_animation(-1)
            else:
                self.change_animation(1)
            return
        if event.key() == Qt.Key_F1:
            # F1键，导入图片
            self.load_images()
        elif event.key() == Qt.Key_F2:
            # F2键，播放/暂停
            if self.animation_mode:
                if self.play_timer.isActive():
                    self.play_timer.stop()
                    self.is_playing = False
                    self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
                    self.pause_action.setText("播放")
                else:
                    self.is_playing = True
                    self.play_timer.start(self._auto_anim_interval_ms)
                    self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
                    self.pause_action.setText("暂停")
            else:
                self.toggle_play()
        elif event.key() == Qt.Key_F7:
            if self.animation_mode and not self.is_playing and self.animation_sets:
                try:
                    self.display_animation_difference()
                except Exception:
                    pass
            else:
                self.generate_report()
        elif not self.images:
            return
        elif event.key() == Qt.Key_Left:
            # 左方向键，显示上一张图片
            self.change_image(-1)
        elif event.key() == Qt.Key_Right:
            # 右方向键，显示下一张图片
            self.change_image(1)
        elif event.key() == Qt.Key_Up:
            # 上方向键，显示上一张图片
            self.change_image(-1)
        elif event.key() == Qt.Key_Down:
            # 下方向键，显示下一张图片
            self.change_image(1)
        elif event.key() == Qt.Key_Delete:
            # Delete键，直接删除当前图片（对叠加图不生效）
            if not self.is_stacked and self.images and 0 <= self.current_index < len(self.images):
                # 保存要删除的图像文件名，用于清理相关记录
                removed_image = os.path.basename(self.images[self.current_index]['filename'])
                
                # 从数据中删除
                del self.images[self.current_index]
                if self.current_index < len(self.original_images):
                    del self.original_images[self.current_index]
                
                # 更新当前索引
                if self.images:
                    # 调整当前索引，确保不越界
                    self.current_index = min(self.current_index, len(self.images) - 1)
                    # 显示新的当前图片
                    self.show_current_image()
                else:
                    # 清空显示
                    self.image_view.scene.clear()
                    self.coord_label.setText("X: 0, Y: 0")
                    self.filename_label.setText("尚未加载图片")
                    self.disable_buttons()
                
                # 清理与删除图像相关的点击记录
                self.clean_records_for_removed_images([removed_image])
        # ESC键处理已移除
        else:
            super().keyPressEvent(event)
            
    # 全屏功能已移除

    def change_image(self, direction):
        """切换图片，direction为正表示下一张，为负表示上一张"""
        try:
            if not self.images:
                return
            
            # 如果当前处于叠加模式，则不允许切换图片
            if self.is_stacked:
                return
                
            # 保存当前缩放因子
            current_zoom = self.zoom_slider.value()
                
            # 计算新的索引
            self.current_index = (self.current_index + direction) % len(self.images)
            
            # 更新image_processor以处理当前图像
            current_image = self.images[self.current_index]
            if current_image['filename'].lower().endswith(('.fits', '.fit', '.fts')):
                try:
                    # 初始化或重新加载image_processor
                    if not hasattr(self, 'image_processor') or self.image_processor is None:
                        from app import FitsImageProcessor
                        self.image_processor = FitsImageProcessor()
                    
                    # 加载当前图像到image_processor
                    self.image_processor.load_fits(current_image['filename'])
                except Exception as e:
                    print(f"加载图像到image_processor时出错: {e}")
            
            # 显示图像
            self.show_current_image()
            
            # 应用之前的缩放因子
            self.update_zoom(current_zoom)
            
            # 如果在全屏模式下，也更新全屏窗口的图像
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                fullscreen_view = self.fullscreen_window.centralWidget()
                if isinstance(fullscreen_view, ImageGraphicsView):
                    # 获取当前图像
                    image = self.images[self.current_index]
                    if 'pixmap' in image:
                        # 设置图像
                        fullscreen_view.set_image(image['pixmap'], filename=os.path.basename(image['filename']))
                        
                        # 应用颜色滤镜
                        if hasattr(self, 'color_filter') and self.color_filter:
                            fullscreen_view.set_color_filter(self.color_filter)
                        
                        # 重新绘制标记
                        fullscreen_view.clear_markers()
                        for record in self.click_records:
                            if record['filename'] == os.path.basename(image['filename']):
                                coords = record['coordinates'].strip('()')
                                coords = coords.replace(' ', '')  # 移除可能的空格
                                try:
                                    x, y = map(int, coords.split(','))
                                    fullscreen_view.add_marker(QPointF(x, y))
                                except ValueError:
                                    print(f"无法解析坐标: {record['coordinates']}")
                        
                        # 显示报告标记
                        if hasattr(self, 'report_markers'):
                            for marker in self.report_markers:
                                if marker['image_filename'] == image['filename']:
                                    # 创建黄色圆圈标记
                                    pos = QPointF(marker['x'], marker['y'])
                                    size = 15  # 圆形大小
                                    circle = fullscreen_view.scene.addEllipse(
                                        pos.x() - size/2, pos.y() - size/2, 
                                        size, size, 
                                        QPen(QColor('yellow'), 1.5),  # 黄色线条
                                        QBrush(Qt.transparent)  # 中心透明
                                    )
                                    circle.setZValue(20)  # 确保在最上层
                                    
                                    # 存储临时标记
                                    if not hasattr(fullscreen_view, 'report_circle_markers'):
                                        fullscreen_view.report_circle_markers = []
                                    fullscreen_view.report_circle_markers.append(circle)
        except Exception as e:
            print(f"切换图片时出错: {e}")
            self.statusBar().showMessage(f"切换图片失败: {e}")
            
        # 动画切换时更新放大镜内容
        if hasattr(self, 'image_view') and self.image_view.magnifier_enabled:
            try:
                # 获取当前鼠标位置
                cursor_pos = QCursor.pos()
                # 将全局坐标转换为视图坐标
                view_pos = self.image_view.viewport().mapFromGlobal(cursor_pos)
                # 将视图坐标转换为场景坐标
                scene_pos = self.image_view.mapToScene(view_pos)
                self.image_view._update_magnifier_content(scene_pos)
            except Exception:
                pass
          

    def show_current_image(self):
        """显示当前选中的图像"""
        if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
            # 重置文件名标签
            self.filename_label.setText("尚未加载图片")
            return
            
        try:
            # 如果当前处于叠加模式，切换回普通模式
            if self.is_stacked:
                self.is_stacked = False
                self.stack_action.setChecked(False)
            
            # 保存当前的辅助线绘制状态和颜色滤镜
            drawing_line_mode = False
            color_filter = None
            if self.image_view:
                try:
                    drawing_line_mode = self.image_view.drawing_auxiliary_line
                    color_filter = self.image_view.color_filter
                    # 确保清除临时标记点
                    self.image_view.clear_temp_markers()
                except Exception as e:
                    print(f"获取图像视图状态出错: {e}")
                
            # 获取当前图像
            image = self.images[self.current_index]
            
            # 清除现有标记
            try:
                self.image_view.clear_markers()
            except Exception as e:
                print(f"清除标记出错: {e}")
            
            # 设置图像并显示文件名
            try:
                # 检查是否需要重新加载图像（例如伪平场处理后）
                if image['filename'].lower().endswith(('.fits', '.fts', '.fit')):
                    # 对于FITS文件，使用缓存机制加载和处理
                    try:
                        # 记录开始时间，用于性能分析
                        start_time = time.time()
                        
                        # 检查图像是否已有缓存的pixmap
                        if 'pixmap' in image and image['pixmap'] is not None:
                            # 如果已有pixmap，直接使用
                            logging.debug(f"使用缓存的pixmap: {os.path.basename(image['filename'])}")
                            pixmap_from_cache = True
                        else:
                            pixmap_from_cache = False
                            # 检查图像处理器是否已加载该文件
                            current_file_loaded = False
                            if hasattr(self, 'image_processor') and hasattr(self.image_processor, 'image_path'):
                                current_file_loaded = (self.image_processor.image_path == image['filename'])
                            
                            # 仅在没有缓存pixmap时重新加载
                            if not pixmap_from_cache:
                                # 确保image_processor已初始化
                                if not hasattr(self, 'image_processor') or self.image_processor is None:
                                    self.image_processor = FitsImageProcessor()
                                
                                # 设置WCS自动解析选项（可以根据用户设置调整）
                                # 默认启用WCS解析，但对于大型图像可以考虑禁用以提高性能
                                if hasattr(self, 'auto_parse_wcs'):
                                    self.image_processor.set_auto_parse_wcs(self.auto_parse_wcs)
                                
                                # 加载FITS文件（使用缓存机制）
                                load_time_start = time.time()
                                load_result = self.image_processor.load_fits(image['filename'])
                                load_time = time.time() - load_time_start
                                logging.debug(f"FITS加载耗时: {load_time*1000:.2f}ms")
                                
                                if not load_result or self.image_processor.image_data is None:
                                    raise Exception("无法加载FITS图像数据")
                                
                                # 获取数据
                                data = self.image_processor.image_data
                                
                                # 使用缓存的Screen Stretch配置
                                if not hasattr(self, '_cached_stretch_config'):
                                    config_file = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "screen_stretch_config.json"))
                                    if os.path.exists(config_file):
                                        with open(config_file, "r") as f:
                                            self._cached_stretch_config = json.load(f)
                                    else:
                                        self._cached_stretch_config = {
                                            "min_percent": 1.0,
                                            "max_percent": 99.5,
                                            "stretch_mode": "Unified"
                                        }
                                
                                min_percent = self._cached_stretch_config.get("min_percent", 1.0)
                                max_percent = self._cached_stretch_config.get("max_percent", 99.5)
                                
                                # 使用缓存的百分比值计算
                                # 检查是否有缓存的计算结果
                                stretch_time_start = time.time()
                                cache_key = f"{image['filename']}_{min_percent}_{max_percent}"
                                if hasattr(self, '_stretch_cache') and cache_key in self._stretch_cache:
                                    # 使用缓存的计算结果
                                    min_val, max_val = self._stretch_cache[cache_key]
                                    logging.debug(f"使用缓存的stretch计算结果: {os.path.basename(image['filename'])}")
                                else:
                                    # 计算新的值并缓存
                                    flat_data = data.flatten()
                                    min_val = np.percentile(flat_data, min_percent)
                                    max_val = np.percentile(flat_data, max_percent)
                                    
                                    # 创建或更新缓存
                                    if not hasattr(self, '_stretch_cache'):
                                        self._stretch_cache = {}
                                    self._stretch_cache[cache_key] = (min_val, max_val)
                                    
                                    # 限制缓存大小
                                    if len(self._stretch_cache) > 200:  # 增加缓存容量到200个计算结果
                                        # 移除最早的缓存项
                                        keys = list(self._stretch_cache.keys())
                                        for old_key in keys[:len(keys)-200]:  # 保留最新的200个
                                            del self._stretch_cache[old_key]
                                
                                # 应用拉伸
                                if max_val > min_val:
                                    stretched_data = np.clip(data, min_val, max_val)
                                    normalized_data = ((stretched_data - min_val) / (max_val - min_val) * 255).astype(np.uint8)
                                else:
                                    normalized_data = np.zeros_like(data, dtype=np.uint8)
                                
                                stretch_time = time.time() - stretch_time_start
                                logging.debug(f"Stretch处理耗时: {stretch_time*1000:.2f}ms")
                                
                                # 创建QImage
                                qimage_time_start = time.time()
                                if len(normalized_data.shape) == 2:  # 2D灰度图像
                                    height, width = normalized_data.shape
                                    qimage = QImage(normalized_data.data, width, height, width, QImage.Format_Grayscale8)
                                else:  # 3D彩色图像
                                    height, width = normalized_data.shape[:2]
                                    qimage = QImage(normalized_data.data, width, height, width*3, QImage.Format_RGB888)
                                
                                # 更新pixmap
                                image['pixmap'] = QPixmap.fromImage(qimage)
                                qimage_time = time.time() - qimage_time_start
                                logging.debug(f"QImage创建耗时: {qimage_time*1000:.2f}ms")
                        
                        total_time = time.time() - start_time
                        logging.debug(f"图像处理总耗时: {total_time*1000:.2f}ms, 使用缓存pixmap: {pixmap_from_cache}")
                    except Exception as e:
                        print(f"加载FITS文件出错: {e}")
                        import traceback
                        traceback.print_exc()
                
                self.image_view.set_image(image['pixmap'], filename=os.path.basename(image['filename']))
                self.image_view.current_image = image  # 存储当前图片引用
                self.image_view.restore_auxiliary_lines()  # 加载对应辅助线
                
                # 初始化WCS信息 - 使用缓存机制加载WCS信息
                if hasattr(self, 'image_processor'):
                    # 检查当前文件是否已加载
                    current_file_loaded = False
                    if hasattr(self.image_processor, 'image_path'):
                        current_file_loaded = (self.image_processor.image_path == image['filename'])
                    
                    # 只有当前文件未加载时才重新加载
                    if not current_file_loaded:
                        # 加载新图片的WCS信息（使用缓存机制）
                        self.image_processor.load_fits(image['filename'])

                try:
                    if hasattr(self, 'auto_parse_wcs') and self.auto_parse_wcs:
                        if not image.get('auto_parsed', False):
                            if self.image_processor and getattr(self.image_processor, 'wcs', None):
                                from coordinate_parser import CoordinateParser
                                from app import SettingsManager
                                sm = SettingsManager()
                                cp = CoordinateParser(sm)
                                res = cp.parse_image_coordinates(self.image_processor, None)
                                if res:
                                    image['auto_parsed'] = True
                                    image['parsed_coords'] = res
                                    self.statusBar().showMessage(
                                        f"自动解析完成: 中心 RA={res.get('center_ra_hms','')}, DEC={res.get('center_dec_dms','')} | FOV {res.get('fov_width',0):.2f}°×{res.get('fov_height',0):.2f}° | 像素 {res.get('pixel_scale',0):.2f} arcsec/pixel",
                                        5000
                                    )
                            else:
                                self.statusBar().showMessage("正在自动解析坐标...")
                                self.auto_parse_thread = AutoParseThread(self.image_processor)
                                self.auto_parse_thread.progress_signal.connect(lambda s: self.statusBar().showMessage(s))
                                def _on_auto_parsed(res):
                                    try:
                                        if res:
                                            image['auto_parsed'] = True
                                            image['parsed_coords'] = res
                                            self.statusBar().showMessage(
                                                f"自动解析完成: 中心 RA={res.get('center_ra_hms','')}, DEC={res.get('center_dec_dms','')} | FOV {res.get('fov_width',0):.2f}°×{res.get('fov_height',0):.2f}° | 像素 {res.get('pixel_scale',0):.2f} arcsec/pixel",
                                                5000
                                            )
                                        else:
                                            self.statusBar().showMessage("自动解析失败或未返回结果")
                                    except Exception:
                                        pass
                                def _on_auto_error(err):
                                    self.statusBar().showMessage(f"自动解析错误: {err}")
                                self.auto_parse_thread.result_signal.connect(_on_auto_parsed)
                                self.auto_parse_thread.error_signal.connect(_on_auto_error)
                                self.auto_parse_thread.start()
                except Exception:
                    pass

                # 恢复辅助线 - 修复辅助线在动画或切换图像时消失的问题
                try:
                    self.image_view.restore_auxiliary_lines()
                except Exception as e:
                    print(f"恢复辅助线出错: {e}")
                
                # 更新状态栏中的文件名标签，显示不带后缀的文件名
                filename = os.path.basename(image['filename'])
                filename_without_ext = os.path.splitext(filename)[0]
                self.filename_label.setText(filename_without_ext)
                
                # 确保文件名标签在状态栏显示区域优先可见
                QTimer.singleShot(1000, lambda: self.filename_label.show())
            except Exception as e:
                print(f"设置图像出错: {e}")
                # 尝试简单地设置图像，不带文件名
                try:
                    self.image_view.scene.clear()
                    self.image_view.pixmap_item = self.image_view.scene.addPixmap(image['pixmap'])
                    
                    # 恢复辅助线 - 即使使用备用方法设置图像，也尝试恢复辅助线
                    try:
                        self.image_view.restore_auxiliary_lines()
                    except Exception as e:
                        print(f"备用方法恢复辅助线出错: {e}")
                except Exception as inner_e:
                    print(f"备用设置图像也失败: {inner_e}")
                    raise  # 重新抛出异常
            
            # 恢复辅助线绘制状态
            if drawing_line_mode:
                try:
                    self.image_view.start_drawing_line()
                except Exception as e:
                    print(f"恢复辅助线绘制状态出错: {e}")
            
            # 恢复颜色滤镜
            if color_filter:
                try:
                    self.image_view.set_color_filter(color_filter)
                except Exception as e:
                    print(f"应用颜色滤镜出错: {e}")
                
            # 更新缩放标签
            self.zoom_label.setText(f'缩放: {self.image_view.zoom_factor:.1f}x')
            
            # 确保文件名在状态栏保持可见
            filename = os.path.basename(image['filename'])
            filename_without_ext = os.path.splitext(filename)[0]
            self.filename_label.setText(filename_without_ext)
            
            try:
                for record in self.click_records:
                    if record['filename'] == os.path.basename(image['filename']):
                        coords = record['coordinates'].strip('()')
                        x, y = map(int, coords.split(','))
                        self.image_view.add_marker(QPointF(x, y))
                if hasattr(self, 'report_markers'):
                    for marker in self.report_markers:
                        if marker['image_filename'] == image['filename']:
                            self.add_report_marker_to_current_image(marker['x'], marker['y'])
                if 'asteroid_overlay_pixmap' in image and image['asteroid_overlay_pixmap']:
                    try:
                        self.image_view.set_asteroid_overlay_pixmap(image['asteroid_overlay_pixmap'])
                        if self.asteroid_markers_hidden and self.image_view.asteroid_overlay_item:
                            self.image_view.asteroid_overlay_item.setVisible(False)
                    except Exception:
                        pass
                # 对于数量较少的标记，补充绘制矢量标记项，保证可见性
                try:
                    if 'asteroid_markers' in image and image['asteroid_markers']:
                        am = image['asteroid_markers']
                        if isinstance(am, list) and len(am) <= 50:
                            for m in am:
                                self.image_view.add_marker(m)
                            if self.asteroid_markers_hidden:
                                for mk in getattr(self.image_view, 'markers', []):
                                    if mk.get('type') == 'asteroid' and mk.get('item'):
                                        mk['item'].setVisible(False)
                except Exception:
                    pass
            except Exception as e:
                print(f"重新绘制标记出错: {e}")
                
            # 如果在全屏模式下，也更新全屏窗口的图像
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                fullscreen_view = self.fullscreen_window.centralWidget()
                if isinstance(fullscreen_view, ImageGraphicsView):
                    try:
                        # 设置图像
                        fullscreen_view.set_image(image['pixmap'], filename=os.path.basename(image['filename']))
                        
                        # 恢复辅助线 - 全屏模式下也恢复辅助线
                        try:
                            fullscreen_view.restore_auxiliary_lines()
                        except Exception as e:
                            print(f"全屏模式恢复辅助线出错: {e}")
                        
                        # 应用颜色滤镜
                        if color_filter:
                            fullscreen_view.set_color_filter(color_filter)
                        
                        # 重新绘制标记
                        fullscreen_view.clear_markers()
                        for record in self.click_records:
                            if record['filename'] == os.path.basename(image['filename']):
                                coords = record['coordinates'].strip('()')
                                coords = coords.replace(' ', '')  # 移除可能的空格
                                try:
                                    x, y = map(int, coords.split(','))
                                    fullscreen_view.add_marker(QPointF(x, y))
                                except ValueError:
                                    print(f"无法解析坐标: {record['coordinates']}")
                    except Exception as e:
                            print(f"更新全屏窗口图像出错: {e}")
                        
            # 如果放大镜已启用，更新其内容
            if hasattr(self, 'image_view') and self.image_view.magnifier_enabled:
                try:
                    # 获取当前鼠标位置
                    cursor_pos = QCursor.pos()
                    # 将全局坐标转换为视图坐标
                    view_pos = self.image_view.viewport().mapFromGlobal(cursor_pos)
                    # 将视图坐标转换为场景坐标
                    scene_pos = self.image_view.mapToScene(view_pos)
                    self.image_view._update_magnifier_content(scene_pos)
                except Exception:
                    pass
                        
        except Exception as e:
            # 捕获并显示所有未捕获的异常
            import traceback
            error_detail = traceback.format_exc()
            print(f"显示图像时出错: {e}\n{error_detail}")
            QMessageBox.critical(self, "错误", f"显示图像时出错: {str(e)}")

    def on_image_selected(self, index):
        """处理图像选择事件"""
        if index >= 0 and index < len(self.images):
            self.current_index = index
            self.show_current_image()
            dialog = self.sender().parent()
            if dialog and isinstance(dialog, QDialog):
                dialog.accept()  # 关闭对话框
    
    def update_coordinates(self, pos):
        """更新坐标显示"""
        x, y = int(pos.x()), int(pos.y())
        coord_text = f"X: {x}, Y: {y}"
        pixel_value = None
        
        # 如果当前图像是FITS文件，尝试显示像素值
        if self.images and self.current_index >= 0 and self.current_index < len(self.images):
            current_image = self.images[self.current_index]
            filename = current_image['filename']
            if filename.lower().endswith(('.fits', '.fit', '.fts')):
                try:
                    # 检查是否有image_processor实例
                    if hasattr(self, 'image_processor') and self.image_processor:
                        # 确保当前加载的是正确的文件
                        current_file_loaded = True
                        if hasattr(self.image_processor, 'current_file'):
                            current_file_loaded = (self.image_processor.current_file == filename)
                        
                        # 如果当前文件未加载，尝试加载它
                        if not current_file_loaded:
                            try:
                                self.image_processor.load_fits(filename)
                                if hasattr(self, 'coord_parser') and self.coord_parser:
                                    self.coord_parser.parse_image_coordinates(self.image_processor, 
                                                                            lambda msg: self.statusBar().showMessage(msg))
                            except Exception as e:
                                print(f"加载当前文件出错: {e}")
                        
                        # 获取像素值
                        if hasattr(self.image_processor, 'image_data') and self.image_processor.image_data is not None:
                            height, width = self.image_processor.image_data.shape[:2]
                            if 0 <= x < width and 0 <= y < height:
                                try:
                                    pixel_value = self.image_processor.get_pixel_value(x, y)
                                    coord_text = f"X: {x}, Y: {y}, 值: {pixel_value:.2f}"
                                except:
                                    pass
                                
                                # 如果有WCS信息，添加天文坐标
                                if hasattr(self.image_processor, 'wcs') and self.image_processor.wcs:
                                    try:
                                        # 使用WCS将像素坐标转换为天文坐标
                                        sky_coord = self.image_processor.wcs.pixel_to_world(x, y)
                                        ra = sky_coord.ra.deg
                                        dec = sky_coord.dec.deg
                                        
                                        # 将RA和DEC转换为更易读的格式
                                        if hasattr(self.image_processor, 'ra_deg_to_hms') and hasattr(self.image_processor, 'dec_deg_to_dms'):
                                            ra_hms = self.image_processor.ra_deg_to_hms(ra)
                                            dec_dms = self.image_processor.dec_deg_to_dms(dec)
                                            
                                            # 同时显示度数和时分秒格式
                                            if pixel_value is not None:
                                                coord_text = f"RA: {ra_hms} ({ra:.4f}°), DEC: {dec_dms} ({dec:.4f}°), X: {x}, Y: {y}, 值: {pixel_value:.2f}"
                                            else:
                                                coord_text = f"RA: {ra_hms} ({ra:.4f}°), DEC: {dec_dms} ({dec:.4f}°), X: {x}, Y: {y}"
                                        else:
                                            if pixel_value is not None:
                                                coord_text = f"RA: {ra:.6f}°, DEC: {dec:.6f}°, X: {x}, Y: {y}, 值: {pixel_value:.2f}"
                                            else:
                                                coord_text = f"RA: {ra:.6f}°, DEC: {dec:.6f}°, X: {x}, Y: {y}"
                                    except Exception as e:
                                        # 坐标转换失败，保持原有显示
                                        print(f"WCS转换出错: {e}")
                except Exception as e:
                    # 出错时不显示像素值，保持原有坐标显示
                    print(f"获取像素值出错: {e}")
        
        self.coord_label.setText(coord_text)
    
    def update_zoom_label(self, zoom_factor):
        """更新缩放比例标签"""
        self.zoom_label.setText(f'缩放: {zoom_factor:.1f}x')
    
    def on_image_clicked(self, pos):
        """处理图像点击事件，仅用于删除标记点"""
        if not self.images:
            return
            
        # 检查是否点击了已标记的点
        threshold = 10  # 点击判定阈值（像素）
        for marker in self.image_view.markers:
            marker_pos = marker['pos']
            distance = ((pos.x() - marker_pos.x()) ** 2 + (pos.y() - marker_pos.y()) ** 2) ** 0.5
            
            if distance <= threshold:
                # 找到对应的记录并删除
                for i, record in enumerate(self.click_records):
                    if (record['coordinates'] == f"({int(marker_pos.x())}, {int(marker_pos.y())})" and
                        os.path.basename(self.images[self.current_index]['filename']) == record['filename']):
                        self.click_records.pop(i)
                        break
                
                # 删除标记点
                self.image_view.markers.remove(marker)
                self.image_view.scene.removeItem(marker['item'])
                
                # 如果在全屏模式下，也从全屏视图中删除标记
                if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                    fullscreen_view = self.fullscreen_window.centralWidget()
                    if isinstance(fullscreen_view, ImageGraphicsView):
                        # 在全屏视图中查找并删除对应的标记
                        for fs_marker in fullscreen_view.markers[:]:
                            fs_marker_pos = fs_marker['pos']
                            if (int(fs_marker_pos.x()) == int(marker_pos.x()) and 
                                int(fs_marker_pos.y()) == int(marker_pos.y())):
                                fullscreen_view.markers.remove(fs_marker)
                                fullscreen_view.scene.removeItem(fs_marker['item'])
                                break
                
                self.update_record_display()
                return
                
        # 不再处理添加新标记的逻辑
        # 单击时不添加标记，只有双击时才添加标记

    def update_record_display(self):
        """更新记录显示"""
        # 在新的UI结构中，我们不再有record_tree控件
        # 这个方法保留为空，以便兼容现有代码
        pass
    
    def show_record_menu(self, position):
        """显示记录右键菜单"""
        # 在新的UI结构中，我们不再有record_tree控件
        # 这个方法保留为空，以便兼容现有代码
        pass
    
    def delete_selected_record(self):
        """删除选中的记录"""
        # 在新的UI结构中，我们不再有record_tree控件
        # 这个方法保留为空，以便兼容现有代码
        pass
    
    def copy_selected_record(self):
        """复制选中的记录"""
        # 在新的UI结构中，我们不再有record_tree控件
        # 这个方法保留为空，以便兼容现有代码
        pass
    
    def generate_report(self):
        """生成报告"""
        try:
            if not self.images or not self.click_records:
                QMessageBox.warning(self, "警告", "没有可用的记录！")
                return
                
            # 检查是否有带有时间信息的记录
            records_with_time = [record for record in self.click_records 
                               if record['time'] != "时间信息未知"]
            
            if not records_with_time:
                QMessageBox.warning(self, "警告", "所有记录均无有效时间信息，无法生成标准格式报告！")
                # 显示一个只包含坐标的简单报告
                simple_report = self.generate_simple_report()
                # 查找报告对话框中的文本编辑器
                for widget in QApplication.topLevelWidgets():
                    if isinstance(widget, QDialog) and widget.windowTitle() == "报告展示":
                        for child in widget.findChildren(QTextEdit):
                            child.setPlainText(simple_report)
                            break
                        break
                return
                
            # 获取当前图片的分辨率
            current_image = self.images[self.current_index]['pixmap']
            
            # 创建报告内容 - 修改格式
            # 1. 分辨率行
            report_content = f"Picture size: {current_image.width()} x {current_image.height()}\n\n"
            # 2. 第三行顶格显示Upper Left
            report_content += "Upper Left\n"
            
            # 只使用有有效时间信息的记录，并按时间排序
            sorted_records = sorted(records_with_time, key=lambda x: x['timestamp'])
            
            # 重新格式化日期和坐标
            formatted_records = []
            for record in sorted_records:
                # 原格式: '2025-03-10 03:45:25'，转换为 '20250310  0345'
                try:
                    timestamp = record['timestamp']
                    # 格式化日期: YYYYMMDD
                    date_part = timestamp.strftime('%Y%m%d')
                    # 格式化时间: HHMM (去掉秒)
                    time_part = timestamp.strftime('%H:%M:%S')
                    
                    # 提取坐标，去掉括号和逗号
                    coord_str = record['coordinates'].strip('()')
                    x, y = map(int, coord_str.split(','))
                    coord_formatted = f"{x} {y}"
                    
                    # 将记录添加到列表中 - 日期在一行，时间和坐标在下一行(时间和坐标之间空两格)
                    formatted_records.append({
                        'date': date_part,
                        'time': time_part,
                        'coordinates': coord_formatted
                    })
                except Exception as e:
                    print(f"格式化记录时出错: {e}")
            
            # 添加记录到报告
            if formatted_records:
                current_date = ""
                for record in formatted_records:
                    date_part = record['date']
                    time_part = record['time']
                    coord = record['coordinates']
                    
                    # 如果日期变了，添加新的日期行
                    if date_part != current_date:
                        current_date = date_part
                        report_content += f"{date_part}\n"
                    
                    # 添加时间和坐标 (时间和坐标之间空两格)
                    report_content += f"{time_part}  {coord}\r\n"
            
            # 查找报告对话框中的文本编辑器
            for widget in QApplication.topLevelWidgets():
                if isinstance(widget, QDialog) and widget.windowTitle() == "报告展示":
                    for child in widget.findChildren(QTextEdit):
                        child.setPlainText(report_content)
                        break
                    break
            
            QMessageBox.information(self, "成功", "报告已生成！")
        except Exception as e:
            print(f"生成报告时出错: {e}")
            QMessageBox.critical(self, "错误", f"生成报告时出错: {e}")
    
    def generate_simple_report(self):
        """生成简单报告（仅包含坐标）"""
        if not self.click_records:
            return "没有点击记录"
            
        # 获取当前图片的分辨率
        current_image = self.images[self.current_index]['pixmap']
        
        # 创建报告内容
        report_content = f"Picture size: {current_image.width()} x {current_image.height()}\n\n"
        report_content += "无时间信息的坐标记录:\n"
        
        # 提取坐标
        for record in self.click_records:
            filename = record['filename']
            
            # 检查是否有WCS坐标
            if 'wcs_coordinates' in record and record['wcs_coordinates'] and 'ra_hms' in record and 'dec_dms' in record:
                # 使用WCS坐标（HH MM SS格式）
                ra_str = record['ra_hms']
                dec_str = record['dec_dms']
                
                # 提取像素坐标
                coord_str = record['coordinates'].strip('()')
                x, y = map(int, coord_str.split(','))
                
                # 组合坐标字符串：先显示RA和DEC，然后是X和Y
                coord_formatted = f"RA: {ra_str}, DEC: {dec_str}, X: {x}, Y: {y}"
                report_content += f"{filename}: {coord_formatted}\n"
            else:
                # 没有WCS信息，只使用原始坐标
                report_content += f"{filename}: {record['coordinates']}\n"
        
        return report_content
    
    def remove_selected_images(self):
        """删除选中的图像"""
        # 在新的UI结构中，我们通过对话框选择要删除的图片
        # 这个方法现在只在show_image_list_dialog中通过remove_selected_images_from_dialog调用
        # 保留为空，以便兼容现有代码
        pass
    
    def toggle_play(self):
        """切换播放状态"""
        if self.animation_mode:
            if not self.animation_sets:
                return
            if self.play_timer.isActive():
                self.play_timer.stop()
                self.is_playing = False
                self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
                self.pause_action.setText("播放")
            else:
                self.is_playing = True
                self.play_timer.start(getattr(self, '_auto_anim_interval_ms', 500))
                self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
                self.pause_action.setText("暂停")
            return
        if not self.images:
            return
            
        # 检查是否正在绘制辅助线
        if not self.is_playing and self.draw_line_action.isChecked():
            # 如果要开始播放，且正在绘制辅助线，先停止辅助线绘制
            self.draw_line_action.setChecked(False)
            self.image_view.stop_drawing_line()
            QMessageBox.information(self, "提示", "已停止辅助线绘制模式以开始播放")
            
        self.is_playing = not self.is_playing
        
        if self.is_playing:
            # 使用已保存的播放速度设置
            # 计算实际速度值 (5-50 -> 0.5-5倍速)
            speed_value = self.current_play_speed / 10.0
            # 计算新的时间间隔 (100毫秒是1x基准值)
            interval = int(100 / speed_value) if speed_value > 0 else 200
            
            # 断开之前的连接
            try:
                self.play_timer.timeout.disconnect()
            except:
                pass
                
            # 连接到正常播放函数（默认向后播放）
            self.play_timer.timeout.connect(lambda: self.change_image(1))
            
            self.play_timer.start(interval)
            
            # 设置播放指示器状态
            self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
            self.pause_action.setText("暂停")
            
            # 更新前进后退按钮状态，指示当前播放方向
            self.play_forward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward))
            self.play_forward_action.setText("▶ 向后播放中")
            self.play_backward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward))
            self.play_backward_action.setText("向前播放")
            
            self.statusBar().showMessage("")
        else:
            self.play_timer.stop()
            
            # 重置所有按钮状态
            self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
            self.pause_action.setText("播放")
            self.play_forward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward))
            self.play_forward_action.setText("向后播放")
            self.play_backward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward))
            self.play_backward_action.setText("向前播放")
    
    def play_backward(self):
        """向前播放（从右向左，即从更新的图片向更早的图片）"""
        if not self.images:
            return
            
        self.is_playing = True
        
        # 使用已保存的播放速度设置
        # 计算实际速度值 (5-50 -> 0.5-5倍速)
        speed_value = self.current_play_speed / 10.0
        # 计算新的时间间隔 (100毫秒是1x基准值)
        interval = int(100 / speed_value) if speed_value > 0 else 200
        
        # 断开之前的连接
        try:
            self.play_timer.timeout.disconnect()
        except:
            pass
        # 连接到向前播放函数
        self.play_timer.timeout.connect(lambda: self.change_image(-1))
        
        self.play_timer.start(interval)
        
        # 设置播放指示器状态
        self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        self.pause_action.setText("暂停")
        
        # 更新前进后退按钮状态，指示当前播放方向
        self.play_backward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward))
        self.play_backward_action.setText("◀ 向前播放中")
        self.play_forward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward))
        self.play_forward_action.setText("向后播放")

    def play_forward(self):
        """向后播放（从左向右，即从更早的图片向更新的图片）"""
        if not self.images:
            return
            
        self.is_playing = True
        
        # 使用已保存的播放速度设置
        # 计算实际速度值 (5-50 -> 0.5-5倍速)
        speed_value = self.current_play_speed / 10.0
        # 计算新的时间间隔 (100毫秒是1x基准值)
        interval = int(100 / speed_value) if speed_value > 0 else 200
        
        # 断开之前的连接
        try:
            self.play_timer.timeout.disconnect()
        except:
            pass
        # 连接到向后播放函数
        self.play_timer.timeout.connect(lambda: self.change_image(1))
        
        self.play_timer.start(interval)
        
        # 设置播放指示器状态
        self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        self.pause_action.setText("暂停")
        
        # 更新前进后退按钮状态，指示当前播放方向
        self.play_forward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward))
        self.play_forward_action.setText("▶ 向后播放中")
        self.play_backward_action.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward))
        self.play_backward_action.setText("向前播放")

    def stop_all(self):
        try:
            if self.play_timer.isActive():
                self.play_timer.stop()
            try:
                self.play_timer.timeout.disconnect()
            except Exception:
                pass
            self.is_playing = False
            self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
            self.pause_action.setText("播放")
            if self.animation_mode:
                self.animation_mode = False
                self.animation_sets = []
                self.current_animation_index = -1
                self._anim_toggle = False
                try:
                    # 重置图像处理器，避免残留的WCS状态影响后续自动解析
                    self.image_processor = FitsImageProcessor()
                    self.image_processor.set_auto_parse_wcs(self.auto_parse_wcs)
                except Exception:
                    pass
                if self.images:
                    self.show_current_image()
                else:
                    if hasattr(self, 'image_view') and self.image_view and self.image_view.scene:
                        self.image_view.scene.clear()
                        self.filename_label.setText("尚未加载图片")
                        self.coord_label.setText("X: 0, Y: 0")
            self.enable_buttons()
            self.statusBar().showMessage("已停止")
        except Exception:
            pass
    
    def next_image(self):
        """显示下一张图像"""
        if not self.images:
            return
            
        # 使用统一的change_image方法确保一致性
        self.change_image(1)
    
    def update_speed(self, value):
        """更新播放速度"""
        try:
            # 保存当前速度设置到实例变量，保证切换方向时能保持速度设置
            self.current_play_speed = value
            self.speed_slider.setValue(value)  # 确保滑块值与实际值一致
            
            # 计算实际速度值 (5-50 -> 0.5-5倍速)
            speed_value = value / 10.0
            
            # 更新播放速度
            if self.is_playing and self.play_timer.isActive():
                # 保存当前的播放方向
                current_direction = 1  # 默认向后播放方向
                
                # 尝试断开当前连接以确定播放方向
                try:
                    # 获取当前连接接收器数量
                    connections = self.play_timer.receivers(self.play_timer.timeout)
                    if connections > 0:
                        # 为了检测方向，我们需要看一下当前连接的函数
                        # 检查一下当前方向（通过试错方法）
                        self.play_timer.timeout.disconnect()
                        
                        try:
                            # 尝试断开向前播放的连接
                            # 如果成功断开，说明当前是向前播放（-1方向）
                            self.play_timer.timeout.disconnect(lambda: self.change_image(-1))
                            current_direction = -1
                        except:
                            # 默认是向后播放（1方向）
                            current_direction = 1
                except Exception as e:
                    print(f"确定播放方向时出错: {e}")
                    # 出错时，默认向后播放
                    current_direction = 1
                
                self.play_timer.stop()
                
                # 计算新的时间间隔 (100毫秒是1x基准值)
                interval = int(100 / speed_value) if speed_value > 0 else 200
                
                # 根据之前的播放方向重新连接timeout信号
                if current_direction < 0:
                    # 向前播放
                    self.play_timer.timeout.connect(lambda: self.change_image(-1))
                    
                    # 更新按钮状态，指示当前播放方向
                    self.play_backward_action.setText("")
                    self.play_forward_action.setText("")
                    
                    self.statusBar().showMessage("")
                else:
                    # 向后播放
                    self.play_timer.timeout.connect(lambda: self.change_image(1))
                    
                    # 更新按钮状态，指示当前播放方向
                    self.play_forward_action.setText("")
                    self.play_backward_action.setText("")
                    
                    self.statusBar().showMessage("")
                
                # 重新启动定时器
                self.play_timer.start(interval)
            else:
                # 如果没有播放，只更新状态栏
                self.statusBar().showMessage(f"播放速度已更新为 {speed_value:.1f} 倍速")
        except Exception as e:
            print(f"更新播放速度时出错: {e}")
            self.statusBar().showMessage(f"更新播放速度失败: {e}")
    
    def apply_image_processing(self):
        """应用图像处理"""
        try:
            if not self.images or not self.original_images:
                return
                
            if self.current_index < 0 or self.current_index >= len(self.original_images):
                return
                
            # 获取当前图像的原始数据
            original_data = self.original_images[self.current_index].copy()
            
            # 直接使用原始数据，不应用曝光调整和反色
            processed_data = original_data
            
            # 应用颜色滤镜 (如果选择了)
            if self.color_filter:
                # 根据颜色滤镜删除相应的颜色通道
                if self.color_filter == "red":
                    processed_data[:, :, 0] = 0  # 去除红色通道
                elif self.color_filter == "green":
                    processed_data[:, :, 1] = 0  # 去除绿色通道
                elif self.color_filter == "blue":
                    processed_data[:, :, 2] = 0  # 去除蓝色通道
                    
            # 将处理后的图像数据转换回QPixmap
            height, width, channel = processed_data.shape
            bytesPerLine = 3 * width
            qImg = QImage(processed_data.data, width, height, bytesPerLine, QImage.Format_RGB888)
            pixmap = QPixmap.fromImage(qImg)
            
            # 更新当前显示的图像
            self.images[self.current_index]['pixmap'] = pixmap
            self.show_current_image()
        except Exception as e:
            print(f"应用图像处理出错: {e}")
            QMessageBox.critical(self, "错误", f"应用图像处理时出错: {e}")
    
    def reset_view(self):
        """重置视图"""
        self.image_view.reset_view()

    def update_history_display(self, record=None):
        """更新历史记录显示"""
        # 由于已删除历史记录UI，此方法不再需要更新UI
        pass
    
    def load_images(self):
        """加载图像文件"""
        # 打开文件对话框选择图像文件
        file_dialog = QFileDialog()
        file_dialog.setFileMode(QFileDialog.ExistingFiles)
        file_dialog.setNameFilter("图像文件 (*.fits *.fit *.fts *.jpg *.jpeg *.png *.tif *.tiff *.gif)")
        
        # 设置上次打开的目录
        if self.current_directory:
            file_dialog.setDirectory(self.current_directory)
        
        if file_dialog.exec_():
            file_paths = file_dialog.selectedFiles()
            
            if not file_paths:
                return
            
            # 新增逻辑：首次加载时自动选择追加模式
            if not self.images:  # 如果当前无图片
                reply = QMessageBox.No  # 默认追加
            else:
                # 否则弹出选择对话框
                reply = QMessageBox.question(
                    self, 
                    '导入选项', 
                    '是否替换当前图像列表？\n选择"是"将替换当前列表，选择"否"将添加到当前列表',
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
                )
                
            # 如果是替换模式，清空当前列表和点击记录
            if reply == QMessageBox.Yes:
                self.images.clear()
                # 清空点击记录
                self.click_records.clear()
                # 清除图像上的所有标记
                if self.image_view:
                    self.image_view.clear_markers()
                original_filenames = set()
            else:
                # 追加模式下检查重复文件
                original_filenames = {os.path.abspath(img['filename']) for img in self.images}
            
            duplicate_count = 0
            
            # 过滤掉已存在的文件路径，并更新重复文件的记录
            filtered_paths = []
            for file_path in file_paths:
                abs_path = os.path.abspath(file_path)
                if abs_path in original_filenames:
                    # 找到并移除重复文件的旧记录
                    self.images = [img for img in self.images if os.path.abspath(img['filename']) != abs_path]
                    filtered_paths.append(file_path)  # 仍然添加新文件，但会覆盖旧记录
                    duplicate_count += 1
                else:
                    filtered_paths.append(file_path)
            
            if duplicate_count > 0:
                QMessageBox.information(self, "提示", f"跳过 {duplicate_count} 个重复文件")
            
            if not filtered_paths:
                return
            
            
            # 保存当前目录
            if file_paths:
                self.current_directory = os.path.dirname(file_paths[0])
                # 立即保存配置，确保目录被记录
                self.save_config()
                
            # 保留现有图像列表
            new_images_count = len(file_paths)
            original_count = len(self.images)
            
            # 重置叠加状态和叠加图像
            self.is_stacked = False
            self.stacked_image = None
            self.stack_action.setChecked(False)
            
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "加载图像", 
                f"加载了 {len(file_paths)} 个图像文件"
            )
            self.update_history_display(history_record)
            
            # 显示进度对话框
            progress = QProgressDialog("正在加载图像...", "取消", 0, len(file_paths), self)
            progress.setWindowTitle("加载中")
            progress.setWindowModality(Qt.WindowModal)
            progress.show()
            
            # 进度计数器
            loaded_count = 0
            
            # 限制同时加载的图像数量，避免内存问题
            max_images_to_load = min(len(file_paths), 100)  # 最多加载100张图片
            if len(file_paths) > max_images_to_load:
                QMessageBox.information(self, "提示", f"选择的图片过多，将只加载前{max_images_to_load}张图片。")
                file_paths = file_paths[:max_images_to_load]
            
            # 自动直方图设置：打开后自动执行，保证速度（抽样计算）
            auto_hist_enabled = True
            low_p = 1.0
            high_p = 99.0

            # 第一步：收集所有FITS文件数据，计算统一的拉伸参数
            fits_files = []
            regular_files = []
            fits_data_combined = []
            
            # 分离FITS文件和常规图像文件
            for file_path in file_paths:
                if file_path.lower().endswith(('.fits', '.fts', '.fit')):
                    fits_files.append(file_path)
                else:
                    regular_files.append(file_path)
            
            # 如果有FITS文件，先加载所有数据并计算统一的拉伸参数
            unified_min_val = None
            unified_max_val = None
            
            if fits_files and not auto_hist_enabled:
                progress.setLabelText("正在计算统一拉伸参数...")
                
                # 使用抽样方式减少内存消耗和计算时间
                sample_values = []
                sample_size = 50000  # 从每个文件抽样最多50000个像素点
                
                # 收集抽样的FITS数据
                for i, file_path in enumerate(fits_files):
                    if progress.wasCanceled():
                        break
                    progress.setValue(int(i * 50 / len(fits_files)))  # 前50%进度用于计算参数
                    
                    try:
                        with fits.open(file_path, memmap=True) as hdul:  # 使用memmap减少内存占用
                            data = hdul[0].data
                            if data is not None:
                                # 抽样策略：如果数据量大，随机采样
                                flat_data = data.flatten()
                                if len(flat_data) > sample_size:
                                    # 随机抽样
                                    indices = np.random.choice(len(flat_data), sample_size, replace=False)
                                    sampled_data = flat_data[indices]
                                else:
                                    sampled_data = flat_data
                                
                                sample_values.extend(sampled_data)
                    except Exception as e:
                        print(f"读取FITS文件 {file_path} 失败: {e}")
                
                # 计算统一的拉伸参数
                if sample_values:
                    # 使用更明亮的默认参数
                    min_percent = 1.0   # 调整为1.0%，使图片更亮
                    max_percent = 99.5  # 调整为99.5%，保持对比度
                    
                    # 直接在抽样数据上计算percentile，无需转换为完整数组
                    sample_values = np.array(sample_values)
                    unified_min_val = np.percentile(sample_values, min_percent)
                    unified_max_val = np.percentile(sample_values, max_percent)
                    
                    print(f"计算统一拉伸参数(抽样{len(sample_values)}点): min_val={unified_min_val:.4f}, max_val={unified_max_val:.4f}")
                    
                    # 保存配置到文件
                    try:
                        config_file = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "screen_stretch_config.json"))
                        config_data = {
                            "min_percent": min_percent,
                            "max_percent": max_percent,
                            "stretch_mode": "Unified"
                        }
                        
                        # 确保目录存在
                        config_dir = os.path.dirname(config_file)
                        if not os.path.exists(config_dir):
                            os.makedirs(config_dir)
                        
                        with open(config_file, "w") as f:
                            json.dump(config_data, f, indent=4)
                        
                        self._cached_stretch_config = config_data
                        print(f"已保存统一拉伸配置文件")
                    except Exception as e:
                        print(f"保存配置文件失败: {e}")
            # 常规彩色图像统一拉伸参数
            unified_min_val_r = None
            unified_max_val_r = None
            unified_min_val_g = None
            unified_max_val_g = None
            unified_min_val_b = None
            unified_max_val_b = None
            if regular_files:
                try:
                    sample_size = 50000
                    sample_r = []
                    sample_g = []
                    sample_b = []
                    for idx, file_path in enumerate(regular_files):
                        if progress.wasCanceled():
                            break
                        try:
                            image = QImage(file_path)
                            if image.isNull():
                                continue
                            w = image.width()
                            h = image.height()
                            image = image.convertToFormat(QImage.Format_RGB32)
                            ptr = image.bits()
                            ptr.setsize(h * w * 4)
                            arr = np.frombuffer(ptr, np.uint8).reshape((h, w, 4))
                            arr = arr[:, :, :3]
                            r = arr[:, :, 0].flatten()
                            g = arr[:, :, 1].flatten()
                            b = arr[:, :, 2].flatten()
                            if len(r) > sample_size:
                                sel = np.random.choice(len(r), sample_size, replace=False)
                                sample_r.extend(r[sel])
                                sample_g.extend(g[sel])
                                sample_b.extend(b[sel])
                            else:
                                sample_r.extend(r)
                                sample_g.extend(g)
                                sample_b.extend(b)
                        except Exception as e:
                            print(f"读取常规图像 {file_path} 失败: {e}")
                    if sample_r:
                        min_percent = 1.0
                        max_percent = 99.5
                        sample_r = np.array(sample_r)
                        sample_g = np.array(sample_g)
                        sample_b = np.array(sample_b)
                        unified_min_val_r = np.percentile(sample_r, min_percent)
                        unified_max_val_r = np.percentile(sample_r, max_percent)
                        unified_min_val_g = np.percentile(sample_g, min_percent)
                        unified_max_val_g = np.percentile(sample_g, max_percent)
                        unified_min_val_b = np.percentile(sample_b, min_percent)
                        unified_max_val_b = np.percentile(sample_b, max_percent)
                except Exception as e:
                    print(f"常规图像统一拉伸参数计算失败: {e}")
            
            # 第二步：批量加载所有图像文件
            progress.setLabelText("正在加载图像...")
            batch_size = 10
            all_files = file_paths
            
            for batch_start in range(0, len(all_files), batch_size):
                batch_end = min(batch_start + batch_size, len(all_files))
                batch_paths = all_files[batch_start:batch_end]
                
                for file_path in batch_paths:
                    if progress.wasCanceled():
                        break
                        
                    progress.setValue(50 + int(loaded_count * 50 / len(all_files)))  # 后50%进度用于加载图像
                    loaded_count += 1
                    
                    filename = os.path.basename(file_path)
                    try:
                        # 处理FITS文件
                        if file_path.lower().endswith(('.fits', '.fts', '.fit')):
                            with fits.open(file_path) as hdul:
                                # 获取数据
                                data = hdul[0].data
                                if data is None:
                                    raise Exception("FITS文件中没有数据")
                                
                                # 自动直方图：抽样快速计算低/高百分位并归一化
                                if auto_hist_enabled:
                                    flat = data.flatten()
                                    sample_size = 100000
                                    if len(flat) > sample_size:
                                        idx = np.random.choice(len(flat), sample_size, replace=False)
                                        flat_sample = flat[idx]
                                    else:
                                        flat_sample = flat
                                    min_val = np.percentile(flat_sample, low_p)
                                    max_val = np.percentile(flat_sample, high_p)
                                    if max_val > min_val:
                                        stretched_data = np.clip(data, min_val, max_val)
                                        normalized_data = ((stretched_data - min_val) / (max_val - min_val) * 255).astype(np.uint8)
                                    else:
                                        normalized_data = np.zeros_like(data, dtype=np.uint8)
                                elif unified_min_val is not None and unified_max_val is not None and unified_max_val > unified_min_val:
                                    stretched_data = np.clip(data, unified_min_val, unified_max_val)
                                    normalized_data = ((stretched_data - unified_min_val) / (unified_max_val - unified_min_val) * 255).astype(np.uint8)
                                else:
                                    min_val = np.percentile(data.flatten(), 1.0)
                                    max_val = np.percentile(data.flatten(), 99.5)
                                    if max_val > min_val:
                                        stretched_data = np.clip(data, min_val, max_val)
                                        normalized_data = ((stretched_data - min_val) / (max_val - min_val) * 255).astype(np.uint8)
                                    else:
                                        normalized_data = np.zeros_like(data, dtype=np.uint8)
                                
                                # 创建QImage
                                if len(normalized_data.shape) == 2:  # 2D灰度图像
                                    height, width = normalized_data.shape
                                    qimage = QImage(normalized_data.data, width, height, width, QImage.Format_Grayscale8)
                                else:  # 3D彩色图像
                                    height, width = normalized_data.shape[:2]
                                    qimage = QImage(normalized_data.data, width, height, width*3, QImage.Format_RGB888)
                                
                                # 提取时间信息
                                time_info = self.extract_time_info(filename)
                                
                                pixmap = QPixmap.fromImage(qimage)
                                
                                # 保存处理后的数据
                                self.original_images.append(normalized_data)
                        else:
                            image = QImage(file_path)
                            if image.isNull():
                                raise Exception(f"无法加载图像: {filename}")
                            width = image.width()
                            height = image.height()
                            image = image.convertToFormat(QImage.Format_RGB32)
                            ptr = image.bits()
                            ptr.setsize(height * width * 4)
                            arr = np.frombuffer(ptr, np.uint8).reshape((height, width, 4))
                            arr = arr[:, :, :3]
                            self.original_images.append(arr)
                            time_info = self.extract_time_info(filename)
                            if auto_hist_enabled:
                                sample_size = 100000
                                def ch_stats(ch):
                                    flat = ch.flatten()
                                    if len(flat) > sample_size:
                                        idx = np.random.choice(len(flat), sample_size, replace=False)
                                        flat = flat[idx]
                                    mn = np.percentile(flat, low_p)
                                    mx = np.percentile(flat, high_p)
                                    return mn, mx
                                r_min, r_max = ch_stats(arr[:, :, 0])
                                g_min, g_max = ch_stats(arr[:, :, 1])
                                b_min, b_max = ch_stats(arr[:, :, 2])
                                r = arr[:, :, 0]
                                g = arr[:, :, 1]
                                b = arr[:, :, 2]
                                r_st = np.clip(r, r_min, r_max)
                                g_st = np.clip(g, g_min, g_max)
                                b_st = np.clip(b, b_min, b_max)
                                r_n = ((r_st - r_min) / max(1e-6, (r_max - r_min)) * 255).astype(np.uint8)
                                g_n = ((g_st - g_min) / max(1e-6, (g_max - g_min)) * 255).astype(np.uint8)
                                b_n = ((b_st - b_min) / max(1e-6, (b_max - b_min)) * 255).astype(np.uint8)
                                norm = np.stack([r_n, g_n, b_n], axis=2)
                                qimage = QImage(norm.data, width, height, width * 3, QImage.Format_RGB888)
                                pixmap = QPixmap.fromImage(qimage)
                            elif (
                                unified_min_val_r is not None and unified_max_val_r is not None and
                                unified_min_val_g is not None and unified_max_val_g is not None and
                                unified_min_val_b is not None and unified_max_val_b is not None
                            ):
                                r = arr[:, :, 0]
                                g = arr[:, :, 1]
                                b = arr[:, :, 2]
                                r_st = np.clip(r, unified_min_val_r, unified_max_val_r)
                                g_st = np.clip(g, unified_min_val_g, unified_max_val_g)
                                b_st = np.clip(b, unified_min_val_b, unified_max_val_b)
                                r_n = ((r_st - unified_min_val_r) / (unified_max_val_r - unified_min_val_r) * 255).astype(np.uint8)
                                g_n = ((g_st - unified_min_val_g) / (unified_max_val_g - unified_min_val_g) * 255).astype(np.uint8)
                                b_n = ((b_st - unified_min_val_b) / (unified_max_val_b - unified_min_val_b) * 255).astype(np.uint8)
                                norm = np.stack([r_n, g_n, b_n], axis=2)
                                qimage = QImage(norm.data, width, height, width * 3, QImage.Format_RGB888)
                                pixmap = QPixmap.fromImage(qimage)
                            else:
                                pixmap = QPixmap(file_path)
                        
                        # 添加到图像列表
                        self.images.append({
                            'filename': file_path,
                            'pixmap': pixmap,
                            'time_info': time_info
                        })
                        
                    except Exception as e:
                        QMessageBox.warning(self, "警告", f"加载图像 {filename} 时出错: {str(e)}")
                
                # 每加载一批图像后，应用事件循环，允许UI响应
                QApplication.processEvents()
                
                if progress.wasCanceled():
                    break
         
                # 对新加载的文件进行排序并与现有列表合并
            def image_sort_key(img):
                filename = os.path.basename(img['filename'])
                parts = filename.split('_')
                if len(parts) >= 3:
                    try:
                        datetime_part = parts[2]
                        return datetime_part
                    except:
                        pass
                return filename
            
            # 仅对新加载的图片进行排序
            new_images_sorted = sorted(self.images[original_count:], key=image_sort_key)
            # 合并旧图片和新图片（保持原有顺序）
            self.images = self.images[:original_count] + new_images_sorted
            # 对所有图片重新排序（包括新加入的）
            self.images.sort(key=lambda x: [int(s) if s.isdigit() else s.lower() for s in re.split(r'(\d+)', os.path.basename(x['filename']))])
            
            # 更新当前显示等后续操作
            self.show_current_image()            
            # 重新组织original_images以匹配合并后的顺序
            temp_originals = self.original_images.copy()
            self.original_images = temp_originals[:original_count]
            for img in self.images[original_count:]:
                filename = os.path.basename(img['filename'])
                original_index = next((i for i, f in enumerate(file_paths) if os.path.basename(f) == filename), None)
                if original_index is not None and (original_count + original_index) < len(temp_originals):
                    self.original_images.append(temp_originals[original_count + original_index])
                        
            # 关闭进度对话框
            progress.close()
            
            # 如果成功加载了图像，显示第一张
            if self.images:
                # 确保图像处理器和坐标解析器已初始化
                if not hasattr(self, 'image_processor') or self.image_processor is None:
                    from app import FitsImageProcessor
                    self.image_processor = FitsImageProcessor()
                    
                if not hasattr(self, 'settings_manager') or self.settings_manager is None:
                    from app import SettingsManager
                    self.settings_manager = SettingsManager()
                if not hasattr(self, 'coord_parser') or self.coord_parser is None:
                    from coordinate_parser import CoordinateParser
                    self.coord_parser = CoordinateParser(self.settings_manager)
                
                self.current_index = 0
                self.show_current_image()
                
                # 启用相关按钮
                self.enable_buttons()
                
                # 更新状态栏
                self.statusBar().showMessage(f"新增加载 {new_images_count} 张图像，总计 {len(self.images)} 张")
                
                # 删除全局设置提醒，直接显示第一张图片
        else:
            QMessageBox.warning(self, "警告", "没有加载任何有效图像！")

    def import_asteroids_local(self):
        try:
            if not hasattr(self, 'image_processor') or self.image_processor is None:
                QMessageBox.information(self, "提示", "未加载图片")
                return
            if not self.image_processor.wcs:
                QMessageBox.information(self, "提示", "当前图片缺少WCS坐标，无法导入小行星数据库")
                return
            cfg = configparser.ConfigParser()
            cfg.read('config.ini')
            base_dir = cfg.get('Settings', 'asteroid_db_path', fallback='')
            mpc_path = os.path.join(base_dir, 'MPCORB.DAT') if base_dir else ''
            try:
                mag_limit = float(cfg.get('Settings', 'asteroid_mag_limit', fallback='21'))
            except Exception:
                mag_limit = None
            if not mpc_path or not os.path.isfile(mpc_path):
                QMessageBox.warning(self, "错误", "未找到本地MPCORB.DAT，请在设置中指定小行星数据库路径")
                return
            data = self.image_processor.image_data
            h, w = data.shape[:2]
            cx, cy = w // 2, h // 2
            sky = self.image_processor.wcs.pixel_to_world(cx, cy)
            ra_deg = float(sky.ra.deg)
            dec_deg = float(sky.dec.deg)
            if not (0.0 <= ra_deg < 360.0) or not (-90.0 <= dec_deg <= 90.0):
                QMessageBox.warning(self, "错误", f"WCS中心坐标无效: RA={ra_deg:.3f}, DEC={dec_deg:.3f}")
                return
            obs_dt = None
            try:
                hdr = self.image_processor.header
                if hdr and 'DATE-OBS' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE-OBS']).replace('Z', '+00:00'))
                elif hdr and 'DATE' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE']).replace('Z', '+00:00'))
            except Exception:
                obs_dt = None
            if obs_dt is None:
                obs_dt = datetime.utcnow()
            pix_scale_arcsec = self.image_processor.get_pixel_scale() or 1.0
            fov_w_deg = (pix_scale_arcsec * w) / 3600.0
            fov_h_deg = (pix_scale_arcsec * h) / 3600.0
            progress_dialog = QDialog(self)
            progress_dialog.setWindowTitle("导入小行星数据库")
            dialog_layout = QVBoxLayout(progress_dialog)
            progress_bar = QProgressBar()
            progress_bar.setRange(0, 0)
            dialog_layout.addWidget(progress_bar)
            status_label = QLabel("")
            dialog_layout.addWidget(status_label)
            progress_dialog.setModal(True)
            progress_dialog.show()
            self.asteroid_import_thread = AsteroidImportThread(mpc_path, obs_dt, ra_deg, dec_deg, fov_w_deg, fov_h_deg, self.image_processor.wcs, mag_limit)
            self.asteroid_import_thread.progress_signal.connect(lambda s: status_label.setText(s))
            def create_overlay_pixmap(width, height, markers):
                try:
                    img = QImage(width, height, QImage.Format_ARGB32)
                    img.fill(QColor(0, 0, 0, 0))
                    painter = QPainter(img)
                    try:
                        pen = QPen(Qt.green)
                        pen.setWidthF(0.75)
                        painter.setPen(pen)
                        font = QFont()
                        font.setPointSize(5)
                        painter.setFont(font)
                        for m in markers:
                            x = int(m.get('x', 0))
                            y = int(m.get('y', 0))
                            label = m.get('label', '')
                            size = 8
                            painter.drawEllipse(QPointF(x, y), size/2, size/2)
                            if label:
                                painter.drawText(x + size/2 + 2, y - size/2 - 2, label)
                        return QPixmap.fromImage(img)
                    finally:
                        painter.end()
                except Exception:
                    return None

            def on_done(markers):
                try:
                    progress_bar.setRange(0, 100)
                    progress_bar.setValue(100)
                    # 过滤越界或无效坐标
                    filtered = []
                    for m in markers:
                        x = m.get('x')
                        y = m.get('y')
                        if x is None or y is None:
                            continue
                        try:
                            xf = float(x)
                            yf = float(y)
                        except Exception:
                            continue
                        if not np.isfinite(xf) or not np.isfinite(yf):
                            continue
                        if 0 <= xf < w and 0 <= yf < h:
                            filtered.append(m)
                    if hasattr(self, 'image_view') and self.image_view:
                        self.image_view.clear_asteroid_overlay()
                        overlay = create_overlay_pixmap(w, h, filtered)
                        if overlay:
                            self.image_view.set_asteroid_overlay_pixmap(overlay)
                        # 少量结果时，额外绘制矢量标记，提升可见性
                        try:
                            if len(filtered) <= 50:
                                for m in filtered:
                                    self.image_view.add_marker(m)
                        except Exception:
                            pass
                    try:
                        img = self.images[self.current_index]
                        img['asteroid_markers'] = list(filtered)
                        img['asteroid_overlay_pixmap'] = overlay if overlay else None
                    except Exception:
                        pass
                    if self.asteroid_markers_hidden:
                        try:
                            if self.image_view.asteroid_overlay_item:
                                self.image_view.asteroid_overlay_item.setVisible(False)
                            for mk in getattr(self.image_view, 'markers', []):
                                if mk.get('type') == 'asteroid' and mk.get('item'):
                                    mk['item'].setVisible(False)
                        except Exception:
                            pass
                    self.statusBar().showMessage(f"导入并标记小行星: {len(filtered)} 个")
                finally:
                    progress_dialog.accept()
            def on_error(err):
                try:
                    QMessageBox.critical(self, "错误", f"导入失败: {err}")
                finally:
                    progress_dialog.reject()
            self.asteroid_import_thread.result_signal.connect(on_done)
            self.asteroid_import_thread.error_signal.connect(on_error)
            self.asteroid_import_thread.start()
        except Exception as e:
            QMessageBox.warning(self, "错误", f"导入小行星失败: {e}")

    def import_comets_local(self):
        try:
            if not hasattr(self, 'image_processor') or self.image_processor is None:
                return
            if not self.image_processor.wcs:
                QMessageBox.information(self, "提示", "当前图片缺少WCS坐标，无法导入彗星数据库")
                return
            cfg = configparser.ConfigParser()
            cfg.read('config.ini')
            base_dir = cfg.get('Settings', 'asteroid_db_path', fallback='')
            comet_path = os.path.join(base_dir, 'CometEls.txt') if base_dir else ''
            if not comet_path or not os.path.isfile(comet_path):
                QMessageBox.warning(self, "错误", "未找到本地CometEls.txt，请在设置中指定小行星数据库路径")
                return
            data = self.image_processor.image_data
            if data is None:
                QMessageBox.information(self, "提示", "当前图片数据为空")
                return
            h, w = data.shape[:2]
            cx, cy = w // 2, h // 2
            sky = self.image_processor.wcs.pixel_to_world(cx, cy)
            ra_deg = float(sky.ra.deg)
            dec_deg = float(sky.dec.deg)
            obs_dt = None
            try:
                hdr = self.image_processor.header
                if hdr and 'DATE-OBS' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE-OBS']).replace('Z', '+00:00'))
                elif hdr and 'DATE' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE']).replace('Z', '+00:00'))
            except Exception:
                obs_dt = None
            if obs_dt is None:
                obs_dt = datetime.utcnow()
            pix_scale_arcsec = self.image_processor.pixel_scale if hasattr(self.image_processor, 'pixel_scale') and self.image_processor.pixel_scale else 1.0
            fov_w_deg = (pix_scale_arcsec * w) / 3600.0
            fov_h_deg = (pix_scale_arcsec * h) / 3600.0
            progress_dialog = QDialog(self)
            progress_dialog.setWindowTitle("导入彗星数据库")
            dialog_layout = QVBoxLayout(progress_dialog)
            progress_bar = QProgressBar()
            progress_bar.setRange(0, 0)
            dialog_layout.addWidget(progress_bar)
            status_label = QLabel("")
            dialog_layout.addWidget(status_label)
            progress_dialog.setModal(True)
            progress_dialog.show()
            self.comet_import_thread = CometImportThread(comet_path, obs_dt, ra_deg, dec_deg, fov_w_deg, fov_h_deg, self.image_processor.wcs, None)
            self.comet_import_thread.progress_signal.connect(lambda s: status_label.setText(s))
            def create_overlay_pixmap(width, height, markers):
                try:
                    img = QImage(width, height, QImage.Format_ARGB32)
                    img.fill(QColor(0, 0, 0, 0))
                    painter = QPainter(img)
                    try:
                        pen = QPen(Qt.green)
                        pen.setWidthF(0.75)
                        painter.setPen(pen)
                        font = QFont()
                        font.setPointSize(5)
                        painter.setFont(font)
                        for m in markers:
                            x = int(m.get('x', 0))
                            y = int(m.get('y', 0))
                            label = m.get('label', '')
                            size = 8
                            painter.drawEllipse(QPointF(x, y), size/2, size/2)
                            if label:
                                painter.drawText(x + size/2 + 2, y - size/2 - 2, label)
                        return QPixmap.fromImage(img)
                    finally:
                        painter.end()
                except Exception:
                    return None
            def on_done(markers):
                try:
                    progress_bar.setRange(0, 100)
                    progress_bar.setValue(100)
                    filtered = []
                    for m in markers:
                        x = m.get('x')
                        y = m.get('y')
                        if x is None or y is None:
                            continue
                        try:
                            xf = float(x)
                            yf = float(y)
                        except Exception:
                            continue
                        if not np.isfinite(xf) or not np.isfinite(yf):
                            continue
                        if 0 <= xf < w and 0 <= yf < h:
                            filtered.append(m)
                    if hasattr(self, 'image_view') and self.image_view:
                        self.image_view.clear_asteroid_overlay()
                        overlay = create_overlay_pixmap(w, h, filtered)
                        if overlay:
                            self.image_view.set_asteroid_overlay_pixmap(overlay)
                        try:
                            if len(filtered) <= 50:
                                for m in filtered:
                                    self.image_view.add_marker(m)
                        except Exception:
                            pass
                    try:
                        img = self.images[self.current_index]
                        img['asteroid_markers'] = list(filtered)
                        img['asteroid_overlay_pixmap'] = overlay if overlay else None
                    except Exception:
                        pass
                    if self.asteroid_markers_hidden:
                        try:
                            if self.image_view.asteroid_overlay_item:
                                self.image_view.asteroid_overlay_item.setVisible(False)
                            for mk in getattr(self.image_view, 'markers', []):
                                if mk.get('type') == 'asteroid' and mk.get('item'):
                                    mk['item'].setVisible(False)
                        except Exception:
                            pass
                    self.statusBar().showMessage(f"导入并标记彗星: {len(filtered)} 个")
                finally:
                    progress_dialog.accept()
            def on_error(err):
                try:
                    QMessageBox.critical(self, "错误", f"导入失败: {err}")
                finally:
                    progress_dialog.reject()
            self.comet_import_thread.result_signal.connect(on_done)
            self.comet_import_thread.error_signal.connect(on_error)
            self.comet_import_thread.start()
        except Exception as e:
            QMessageBox.warning(self, "错误", f"导入彗星失败: {e}")

    def toggle_asteroid_markers_visibility(self, hidden):
        try:
            self.asteroid_markers_hidden = hidden
            if hasattr(self, 'image_view') and self.image_view:
                if self.image_view.asteroid_overlay_item:
                    self.image_view.asteroid_overlay_item.setVisible(not hidden)
                # 同步矢量标记的可见性
                for mk in getattr(self.image_view, 'markers', []):
                    if mk.get('type') == 'asteroid' and mk.get('item'):
                        mk['item'].setVisible(not hidden)
            if hasattr(self, 'toggle_asteroid_btn') and self.toggle_asteroid_btn:
                try:
                    icon = self.style().standardIcon(QStyle.SP_DialogCancelButton if hidden else QStyle.SP_DialogApplyButton)
                    self.toggle_asteroid_btn.setIcon(icon)
                except Exception:
                    pass
        except Exception:
            pass

    def mark_known_asteroids(self):
        try:
            if not hasattr(self, 'image_processor') or self.image_processor is None:
                QMessageBox.information(self, "提示", "未加载图片")
                return
            if not self.image_processor.wcs:
                QMessageBox.information(self, "提示", "当前图片缺少WCS坐标，无法标记")
                return
            data = self.image_processor.image_data
            if data is None:
                QMessageBox.information(self, "提示", "当前图片数据为空")
                return
            h, w = data.shape[:2]
            cx, cy = w // 2, h // 2
            sky = self.image_processor.wcs.pixel_to_world(cx, cy)
            ra_deg = float(sky.ra.deg)
            dec_deg = float(sky.dec.deg)
            obs_dt = None
            try:
                hdr = self.image_processor.header
                if hdr and 'DATE-OBS' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE-OBS']).replace('Z', '+00:00'))
                elif hdr and 'DATE' in hdr:
                    obs_dt = datetime.fromisoformat(str(hdr['DATE']).replace('Z', '+00:00'))
            except Exception:
                obs_dt = None
            if obs_dt is None:
                obs_dt = datetime.utcnow()
            year = obs_dt.year
            month = obs_dt.month
            day = round(obs_dt.day + obs_dt.hour/24 + obs_dt.minute/1440 + obs_dt.second/86400, 2)
            ra_h = int(ra_deg / 15)
            ra_m = int((ra_deg / 15 - ra_h) * 60)
            ra_s = ((ra_deg / 15 - ra_h) * 60 - ra_m) * 60
            dec_sign = '+' if dec_deg >= 0 else '-'
            dec_abs = abs(dec_deg)
            dec_d = int(dec_abs)
            dec_m = int((dec_abs - dec_d) * 60)
            dec_s = ((dec_abs - dec_d) * 60 - dec_m) * 60
            ra_str = f"{ra_h:02d}%20{ra_m:02d}%20{ra_s:05.2f}"
            dec_str = f"{dec_sign}{dec_d:02d}%20{dec_m:02d}%20{dec_s:05.2f}"
            oc = self.image_view.settings.get("Settings", "observatory_code", fallback="N88")
            url = f"http://data.minorplanetcenter.net/cgi-bin/mpcheck.cgi?year={year}&month={month}&day={day}&which=pos&ra={ra_str}&decl={dec_str}&TextArea=&limit=22&oc={oc}&radius=20&sort=d&mot=h&tmot=s&pdes=u&needed=f&ps=n&type=p"
            r = requests.get(url, timeout=30)
            txt = r.text
            lines = []
            try:
                soup = BeautifulSoup(txt, 'html.parser')
                pres = soup.find_all('pre')
                if pres:
                    content = pres[0].get_text('\n')
                    lines = content.splitlines()
                else:
                    lines = txt.splitlines()
            except Exception:
                lines = txt.splitlines()
            found = []
            pat_ra = re.compile(r"(\d{2})\s+(\d{2})\s+(\d{2}\.\d+)")
            pat_dec = re.compile(r"([+\-]\d{2})\s+(\d{2})\s+(\d{2}\.\d+)")
            for ln in lines:
                mra = pat_ra.search(ln)
                mdec = pat_dec.search(ln)
                if mra and mdec:
                    parts = ln.strip().split()
                    name = parts[0]
                    rh = int(mra.group(1))
                    rm = int(mra.group(2))
                    rs = float(mra.group(3))
                    dd = int(mdec.group(1))
                    dm = int(mdec.group(2))
                    ds = float(mdec.group(3))
                    ra = (rh + rm/60 + rs/3600) * 15.0
                    dec = (abs(dd) + dm/60 + ds/3600) * (1 if dd >= 0 else -1)
                    found.append((name, ra, dec))
            if not found:
                QMessageBox.information(self, "提示", "附近未检索到已知小行星")
                return
            self.image_view.clear_markers()
            for name, ra, dec in found:
                try:
                    x, y = self.image_processor.wcs.world_to_pixel(SkyCoord(ra, dec, unit=(u.deg, u.deg)))
                except Exception:
                    coord = SkyCoord(ra * u.deg, dec * u.deg)
                    x, y = self.image_processor.wcs.world_to_pixel(coord)
                self.image_view.add_marker({'x': float(x), 'y': float(y), 'type': 'asteroid', 'label': name})
            self.statusBar().showMessage(f"标记已知小行星: {len(found)} 个")
        except Exception as e:
            QMessageBox.warning(self, "错误", f"标记小行星失败: {e}")
    
    def extract_time_info(self, filename):
        """从文件名或FITS头文件中提取时间信息"""
        # 如果是FITS文件，优先从头文件中获取时间信息
        file_path = None
        for img in self.images:
            if os.path.basename(img['filename']) == filename:
                file_path = img['filename']
                break
                
        if file_path and file_path.lower().endswith(('.fits', '.fit', '.fts')):
            try:
                # 检查是否有image_processor实例并且已加载当前文件
                if hasattr(self, 'image_processor') and self.image_processor and self.image_processor.wcs:
                    # 尝试从FITS头文件中获取时间信息
                    with fits.open(file_path) as hdul:
                        header = hdul[0].header
                        # 尝试多种可能的日期时间关键字
                        date_keywords = ['DATE-OBS', 'DATE', 'DATE_OBS', 'OBSDATE', 'UT-DATE']
                        for keyword in date_keywords:
                            if keyword in header:
                                iso_time = header[keyword]
                                # 如果是UT-DATE，尝试组合UT-TIME
                                if keyword == 'UT-DATE' and 'UT-TIME' in header:
                                    iso_time = f"{iso_time}T{header['UT-TIME']}"
                                
                                try:
                                    # 尝试解析ISO格式日期
                                    if 'T' in iso_time:
                                        dt = datetime.fromisoformat(iso_time)
                                    else:
                                        # 尝试其他常见格式
                                        formats = [
                                            '%Y-%m-%d %H:%M:%S',
                                            '%Y/%m/%d %H:%M:%S',
                                            '%Y-%m-%d',
                                            '%Y/%m/%d'
                                        ]
                                        
                                        for fmt in formats:
                                            try:
                                                dt = datetime.strptime(iso_time, fmt)
                                                break
                                            except ValueError:
                                                continue
                                        else:
                                            continue  # 无法解析，尝试下一个关键字
                                    
                                    return dt.strftime('%Y-%m-%d %H:%M:%S')
                                except Exception:
                                    pass  # 解析失败，继续尝试下一个关键字
            except Exception as e:
                print(f"从FITS头文件获取时间信息出错: {e}")
        
        # 如果从FITS头文件获取失败或不是FITS文件，尝试从文件名中提取
        # 匹配CCOR1_1B_20250305T030025_V00_NC_processed.fits格式
        pattern = r'CCOR1_1B_(\d{8})T(\d{6})_'
        match = re.search(pattern, filename)
        if match:
            date_str = match.group(1)  # 20250305
            time_str = match.group(2)  # 030025
            
            year = date_str[:4]
            month = date_str[4:6]
            day = date_str[6:8]
            
            hour = time_str[:2]
            minute = time_str[2:4]
            second = time_str[4:6]
            
            try:
                dt = datetime(int(year), int(month), int(day), 
                             int(hour), int(minute), int(second))
                return dt.strftime('%Y-%m-%d %H:%M:%S')
            except ValueError:
                pass
                
        # 尝试其他格式 (1B_YYYYMMDDTHHMMSS)
        pattern2 = r'1B_([0-9]{8}T[0-9]{6})'
        match = re.search(pattern2, filename)
        if match:
            time_str = match.group(1)
            try:
                dt = datetime.strptime(time_str, '%Y%m%dT%H%M%S')
                return dt.strftime('%Y-%m-%d %H:%M:%S')
            except ValueError:
                pass
                
        # 尝试新增的格式 (YYYYMMDD_HHMM_*)
        pattern3 = r'^(\d{8})_(\d{2}:?\d{2})_'
        match = re.search(pattern3, filename)
        if match:
            date_str = match.group(1)  # 20220101
            time_str = match.group(2)  # 0233
            
            try:
                year = date_str[:4]
                month = date_str[4:6]
                day = date_str[6:8]
                
                hour = time_str[:2]
                minute = time_str[2:4]
                
                dt = datetime(int(year), int(month), int(day), 
                             int(hour), int(minute))
                # 对于这种格式不显示秒
                return dt.strftime('%Y-%m-%d %H:%M')
            except ValueError:
                pass
                
        # 处理swan_ARecl_sm_20250406_0002_rfa格式
        pattern4 = r'swan_ARecl_sm_(\d{8})_\d+_rfa'
        match = re.search(pattern4, filename)
        if match:
            date_str = match.group(1)  # 20250406
            
            try:
                year = date_str[:4]
                month = date_str[4:6]
                day = date_str[6:8]
                
                dt = datetime(int(year), int(month), int(day))
                # 对于这种格式只显示日期，不显示时间
                return dt.strftime('%Y-%m-%d')
            except ValueError:
                pass
            
        return "时间信息未知"
    
    def enable_buttons(self):
        """启用相关按钮"""
        # 启用导航栏按钮
        self.image_list_action.setEnabled(True)
        self.load_action.setEnabled(True)
        self.play_backward_action.setEnabled(True)
        self.play_forward_action.setEnabled(True)
        self.pause_action.setEnabled(True)
        self.prev_image_action.setEnabled(True)
        self.next_image_action.setEnabled(True)
        self.speed_action.setEnabled(True)
        self.goto_action.setEnabled(True)
        self.zoom_in_action.setEnabled(True)
        self.zoom_out_action.setEnabled(True)
        self.draw_line_action.setEnabled(True)
        self.hide_lines_action.setEnabled(True)
        self.line_settings_action.setEnabled(True)
        self.red_filter_action.setEnabled(True)
        self.green_filter_action.setEnabled(True)
        self.blue_filter_action.setEnabled(True)
        self.reset_filter_action.setEnabled(True)
        self.report_action.setEnabled(True)
        self.stack_action.setEnabled(True)  # 启用叠加功能
        # 叠加设置按钮状态取决于当前是否处于叠加模式
        self.stack_settings_action.setEnabled(self.is_stacked)
        # 启用水平和垂直翻转按钮
        self.horizontal_flip_action.setEnabled(True)
        self.vertical_flip_action.setEnabled(True)
        
        # 启用滑块
        self.speed_slider.setEnabled(True)
        self.zoom_slider.setEnabled(True)
        
    def disable_buttons(self):
        """禁用相关按钮"""
        # 禁用导航栏按钮（除了加载图片按钮）
        self.image_list_action.setEnabled(False)
        self.play_backward_action.setEnabled(False)
        self.play_forward_action.setEnabled(False)
        self.pause_action.setEnabled(False)
        self.prev_image_action.setEnabled(False)
        self.next_image_action.setEnabled(False)
        self.speed_action.setEnabled(False)
        self.goto_action.setEnabled(False)
        self.zoom_in_action.setEnabled(False)
        self.zoom_out_action.setEnabled(False)
        self.draw_line_action.setEnabled(False)
        self.hide_lines_action.setEnabled(False)
        self.line_settings_action.setEnabled(False)
        self.red_filter_action.setEnabled(False)
        self.green_filter_action.setEnabled(False)
        self.blue_filter_action.setEnabled(False)
        self.reset_filter_action.setEnabled(False)
        self.report_action.setEnabled(False)
        self.stack_action.setEnabled(False)  # 禁用叠加功能
        self.stack_settings_action.setEnabled(False)  # 禁用叠加设置
        # 禁用水平和垂直翻转按钮
        self.horizontal_flip_action.setEnabled(False)
        self.vertical_flip_action.setEnabled(False)
        
        # 禁用滑块
        self.speed_slider.setEnabled(False)
        self.zoom_slider.setEnabled(False)

    def closeEvent(self, event):
        """窗口关闭事件处理"""
        # 如果是嵌入模式，不显示确认对话框
        if self.embedded_mode:
            # 清除辅助线
            if hasattr(self, 'image_view') and self.image_view:
                self.image_view.clear_auxiliary_lines()
                
            # 清除QSettings中的辅助线数据
            settings = QSettings("CCOR", "ImageViewer")
            settings.remove("auxiliary_lines")
            settings.sync()
                
            # 保存配置（但不保存速度设置，恢复默认）
            self.save_config()
            
            # 重置播放速度到默认值
            self.speed_slider.setValue(10)
            self.current_play_speed = 10
            
            # 停止播放
            if self.is_playing:
                self.toggle_play()
            
            # 如果全屏窗口存在，关闭它
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                self.fullscreen_window.close()
                self.fullscreen_window = None
                
            event.accept()
            return
            
        # 如果不是嵌入模式，显示确认对话框
        reply = QMessageBox.question(
            self, '确认退出', 
            "确定要关闭图像查看器吗？",
            QMessageBox.Yes | QMessageBox.No, 
            QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            # 清除辅助线
            if hasattr(self, 'image_view') and self.image_view:
                self.image_view.clear_auxiliary_lines()
                
            # 清除QSettings中的辅助线数据
            settings = QSettings("CCOR", "ImageViewer")
            settings.remove("auxiliary_lines")
            settings.sync()
                
            # 保存配置
            self.save_config()
            
            # 重置播放速度到默认值1x
            self.speed_slider.setValue(10)
            self.current_play_speed = 10
            
            # 停止播放
            if self.is_playing:
                self.toggle_play()
                
            # 如果全屏窗口存在，关闭它
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                self.fullscreen_window.close()
                self.fullscreen_window = None
                
            event.accept()
        else:
            event.ignore()

    def load_config(self):
        """加载配置文件"""
        try:
            config_file = os.path.abspath("image_viewer_config.json")
            default_config = {
                "last_directory": "",
                "play_speed": 10,  # 默认1倍速，对应100ms的基准值 (值为10表示1.0倍速)
                "stacking": {
                    "brightness": 85,  # 默认亮度 85%
                    "contrast": 5,     # 默认对比度 5%
                    "mode": "maximum",  # 默认叠加模式：最大值
                    "gamma_value": 1.0,  # 默认伽马值
                    "high_performance": True,  # 默认启用高性能模式
                    "use_negative": False  # 默认不使用反色效果
                }
            }
            
            # 设置默认的全局图片设置
            self.global_brightness_value = 110
            self.global_contrast_value = 100
            self.global_gamma_value = 1.0
            
            if os.path.exists(config_file):
                try:
                    with open(config_file, "r") as f:
                        config = json.load(f)
                        self.current_directory = config.get("last_directory", "")
                        self.current_play_speed = config.get("play_speed", 10)
                        
                        # 加载叠加设置
                        stacking_config = config.get("stacking", {})
                        self.brightness_value = stacking_config.get("brightness", 85)
                        self.contrast_value = stacking_config.get("contrast", 5)
                        self.stacking_mode = stacking_config.get("mode", "maximum")
                        self.gamma_value = stacking_config.get("gamma_value", 1.0)
                        self.high_performance = stacking_config.get("high_performance", True)
                        self.use_negative = stacking_config.get("use_negative", False)
                        
                        # 不再从配置文件加载全局图片设置
                except:
                    # 如果配置文件损坏，使用默认设置
                    self.current_directory = default_config["last_directory"]
                    self.current_play_speed = default_config["play_speed"]
                    self.brightness_value = default_config["stacking"]["brightness"]
                    self.contrast_value = default_config["stacking"]["contrast"]
                    self.stacking_mode = default_config["stacking"]["mode"]
                    self.gamma_value = default_config["stacking"]["gamma_value"]
                    self.high_performance = default_config["stacking"]["high_performance"]
                    self.use_negative = default_config["stacking"]["use_negative"]
                    # 全局设置使用默认值
            else:
                # 如果配置文件不存在，使用默认设置
                self.current_directory = default_config["last_directory"]
                self.current_play_speed = default_config["play_speed"]
                self.brightness_value = default_config["stacking"]["brightness"]
                self.contrast_value = default_config["stacking"]["contrast"]
                self.stacking_mode = default_config["stacking"]["mode"]
                self.gamma_value = default_config["stacking"]["gamma_value"]
                self.high_performance = default_config["stacking"]["high_performance"]
                self.use_negative = default_config["stacking"]["use_negative"]
                # 全局设置使用默认值
        # 添加批量对齐路径配置项
            if not self.config.has_section("BatchAlignment"):
                self.config.add_section("BatchAlignment")
                self.config["BatchAlignment"]["new_folder"] = ""
                self.config["BatchAlignment"]["old_folder"] = ""
                self.config["BatchAlignment"]["output_folder"] = ""                
                # 保存默认配置
                self.save_config()
                
        except Exception as e:
            print(f"加载配置文件时出错: {e}")
            import traceback
            traceback.print_exc()
    
    def save_config(self):
        """保存配置文件"""
        try:
            # 保存config.ini文件
            with open('config.ini', 'w') as f:
                self.config.write(f)
                
            # 保存图像查看器配置
            config_file = os.path.abspath("image_viewer_config.json")
            
            # 创建配置数据，不包含全局图片设置
            config = {
                "last_directory": self.current_directory,
                "play_speed": self.current_play_speed,
                "stacking": {
                    "brightness": self.brightness_value,
                    "contrast": self.contrast_value,
                    "mode": self.stacking_mode,
                    "gamma_value": self.gamma_value,
                    "high_performance": self.high_performance,
                    "use_negative": self.use_negative
                }
                # 移除全局设置的保存，使其仅在当前会话有效
            }
            
            # 写入文件
            with open(config_file, "w") as f:
                json.dump(config, f, indent=4)
                
        except Exception as e:
            print(f"保存配置文件时出错: {e}")
            
    def process_parse(self):
        """解析当前图像的坐标信息"""
        if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
            QMessageBox.warning(self, "警告", "请先加载图像！")
            return
        
        # 获取当前图像信息
        current_image = self.images[self.current_index]
        file_path = current_image['filename']
        
        # 检查文件格式
        if not file_path.lower().endswith(('.fits', '.fts', '.fit')):
            QMessageBox.warning(self, "警告", "只能解析FITS格式的图像！")
            return
            
        # 初始化或重新加载image_processor
        if not hasattr(self, 'image_processor') or self.image_processor is None:
            self.image_processor = FitsImageProcessor()
        
        # 每次都重新加载当前图像到image_processor，确保处理的是当前显示的图像
        try:
            load_result = self.image_processor.load_fits(file_path)
            if not load_result or self.image_processor.image_data is None:
                QMessageBox.warning(self, "警告", "无法加载FITS图像数据！")
                return
        except Exception as e:
            QMessageBox.warning(self, "警告", f"加载图像时出错: {str(e)}")
            return
        
        # 创建进度对话框
        progress_dialog = QProgressDialog("正在解析图像坐标...", "取消", 0, 100, self)
        progress_dialog.setWindowTitle("解析进度")
        progress_dialog.setWindowModality(Qt.WindowModal)
        progress_dialog.setMinimumDuration(0)  # 立即显示
        progress_dialog.setValue(0)
        
        # 创建坐标解析器
        from coordinate_parser import CoordinateParser
        from app import SettingsManager
        settings_manager = SettingsManager()
        coord_parser = CoordinateParser(settings_manager)
        
        # 状态回调函数
        def status_callback(message):
            progress_dialog.setLabelText(message)
            QApplication.processEvents()
        
        # 解析坐标
        try:
            progress_dialog.setValue(10)
            result = coord_parser.parse_image_coordinates(self.image_processor, status_callback)
            progress_dialog.setValue(90)
            
            if not result:
                progress_dialog.close()
                QMessageBox.warning(self, "解析失败", "无法解析图像坐标信息，请检查图像质量或手动输入坐标信息。")
                return
            
            # 显示结果
            info_text = f"中心坐标: RA={result['center_ra_hms']}, DEC={result['center_dec_dms']}\n"
            info_text += f"视场大小: {result['fov_width']:.4f}° × {result['fov_height']:.4f}°\n"
            info_text += f"像素比例: {result['pixel_scale']:.4f} 角秒/像素\n"
            if result['coord_system']:
                info_text += f"坐标系统: {result['coord_system']}"
                if result['equinox']:
                    info_text += f" (分点: {result['equinox']})"
            
            progress_dialog.close()
            QMessageBox.information(self, "解析成功", info_text)
            
            # 更新状态栏显示坐标信息
            self.statusBar().showMessage(f"中心: RA={result['center_ra_hms']}, DEC={result['center_dec_dms']}, 视场: {result['fov_width']:.2f}° × {result['fov_height']:.2f}°", 5000)
            
            # 保存解析结果到历史记录
            if hasattr(self, 'history_logger'):
                history_entry = {
                    "action": "parse_coordinates",
                    "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                    "filename": self.current_file if hasattr(self, 'current_file') else "未知文件",
                    "center_ra": result['center_ra'],
                    "center_dec": result['center_dec'],
                    "fov_width": result['fov_width'],
                    "fov_height": result['fov_height'],
                    "pixel_scale": result['pixel_scale']
                }
                self.history_logger.add_record("解析坐标", "解析了图像坐标信息", history_entry)

            # 解析成功后，自动将WCS写入源FITS文件
            try:
                from app import save_parsed_coordinates_to_fits
                current_image = self.images[self.current_index]
                src_path = current_image['filename']
                try:
                    if hasattr(self.image_processor, 'original_hdul') and self.image_processor.original_hdul:
                        self.image_processor.original_hdul.close()
                except Exception:
                    pass
                ok, msg = save_parsed_coordinates_to_fits(self.image_processor, result, src_path)
                if ok:
                    self.statusBar().showMessage("已将WCS写入源文件", 5000)
                    try:
                        self.image_processor.load_fits(src_path)
                    except Exception:
                        pass
                else:
                    QMessageBox.warning(self, "警告", f"写入WCS失败: {msg}")
            except Exception as e2:
                QMessageBox.warning(self, "警告", f"写入WCS过程出错: {e2}")
                
        except Exception as e:
            progress_dialog.close()
            QMessageBox.critical(self, "错误", f"解析过程中发生错误: {str(e)}")
            import traceback
            traceback.print_exc()
    
    def open_settings(self):
        """打开设置对话框"""
        # 确保Settings部分存在
        if not self.config.has_section('Settings'):
            self.config.add_section('Settings')
        settings_dialog = SettingsDialog(self)
        if settings_dialog.exec_() == QDialog.Accepted:
            # 重新加载设置并应用
            try:
                self.config.read('config.ini')
                auto_flag = self.config.get('Settings', 'auto_parse_wcs', fallback="true").strip().lower()
                self.auto_parse_wcs = (auto_flag in ("true", "1", "yes"))
                if hasattr(self, 'image_processor') and self.image_processor:
                    try:
                        self.image_processor.set_auto_parse_wcs(self.auto_parse_wcs)
                    except Exception:
                        pass
            except Exception:
                self.auto_parse_wcs = False
            
    def show_fits_header(self):
        """显示FITS头文件信息"""
        if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
            QMessageBox.warning(self, "警告", "请先加载FITS图像！")
            return
            
        # 获取当前图像信息
        current_image = self.images[self.current_index]
        file_path = current_image['filename']
        
        # 检查文件格式
        if not file_path.lower().endswith(('.fits', '.fts', '.fit')):
            QMessageBox.warning(self, "警告", "只能查看FITS格式的图像头文件信息！")
            return
            
        try:
            # 读取FITS头文件信息
            with fits.open(file_path) as hdul:
                header = hdul[0].header
                
                # 显示头文件信息对话框
                header_dialog = FitsHeaderDialog(self, header)
                header_dialog.exec_()
                
        except Exception as e:
            QMessageBox.warning(self, "警告", f"读取FITS头文件信息时出错: {str(e)}")
            return
            
    def show_fits_header(self):
        """显示FITS头文件信息"""
        if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
            QMessageBox.warning(self, "警告", "请先加载FITS图像！")
            return
            
        # 获取当前图像信息
        current_image = self.images[self.current_index]
        file_path = current_image['filename']
        
        # 检查文件格式
        if not file_path.lower().endswith(('.fits', '.fts', '.fit')):
            QMessageBox.warning(self, "警告", "只能查看FITS格式的图像头文件信息！")
            return
            
        try:
            # 读取FITS头文件信息
            with fits.open(file_path) as hdul:
                header = hdul[0].header
                
                # 显示头文件信息对话框
                header_dialog = FitsHeaderDialog(self, header)
                header_dialog.exec_()
                
        except Exception as e:
            QMessageBox.warning(self, "警告", f"读取FITS头文件信息时出错: {str(e)}")
            return

    def set_embedded_mode(self, embedded=True):
        """设置嵌入模式"""
        self.embedded_mode = embedded
        
        # 在嵌入模式下调整UI
        if embedded:
            # 可以在这里调整UI以适应嵌入环境
            # 例如隐藏某些控件或调整大小
            pass

    def on_image_double_clicked(self, pos):
        """处理图像双击事件，提取并选择图片中的时间信息"""
        if not self.images:
            return
            
        current_image = self.images[self.current_index]
        filename = os.path.basename(current_image['filename'])
        
        # 从文件名中提取时间信息
        time_info = self.extract_time_info(filename)
        
        # 创建记录字典
        x, y = int(pos.x()), int(pos.y())
        record = {
            'filename': filename,
            'time': time_info,
            'coordinates': f"({x}, {y})",
            'timestamp': datetime.strptime(time_info, 
                                          '%Y-%m-%d %H:%M:%S' if ':' in time_info and time_info.count(':') == 2 else 
                                          '%Y-%m-%d %H:%M' if ':' in time_info else 
                                          '%Y-%m-%d') 
                          if time_info != "时间信息未知" else datetime.now()
        }
        
        # 如果有WCS信息，添加天文坐标
        if hasattr(self, 'image_processor') and self.image_processor and self.image_processor.wcs:
            try:
                # 使用WCS将像素坐标转换为天文坐标
                sky_coord = self.image_processor.wcs.pixel_to_world(x, y)
                ra = sky_coord.ra.deg
                dec = sky_coord.dec.deg
                
                # 格式化赤经坐标（时分秒）
                ra_hour = sky_coord.ra.hour
                ra_h = int(ra_hour)
                ra_m = int((ra_hour - ra_h) * 60)
                ra_s = ((ra_hour - ra_h) * 60 - ra_m) * 60
                
                # 格式化赤纬坐标（度分秒）
                dec_sign = '+' if dec >= 0 else '-'
                dec_d = int(abs(dec))
                dec_m = int((abs(dec) - dec_d) * 60)
                dec_s = ((abs(dec) - dec_d) * 60 - dec_m) * 60
                
                # 构建新格式的坐标字符串，确保时分秒都是两位数，秒的小数部分保留两位
                ra_s_int = int(ra_s)
                ra_s_frac = ra_s - ra_s_int
                ra_s_str = f"{ra_s_int:02d}.{int(ra_s_frac * 100):02d}"
                
                dec_s_int = int(dec_s)
                dec_s_frac = dec_s - dec_s_int
                dec_s_str = f"{dec_s_int:02d}.{int(dec_s_frac * 100):02d}"
                
                # 最终坐标字符串
                ra_str = f"{ra_h:02d} {ra_m:02d} {ra_s_str}"
                dec_str = f"{dec_sign}{dec_d:02d} {dec_m:02d} {dec_s_str}"
                
                record['ra'] = ra
                record['dec'] = dec
                record['ra_hms'] = ra_str
                record['dec_dms'] = dec_str
                record['coordinates'] = f"({x}, {y})"
                record['wcs_coordinates'] = True
            except Exception as e:
                logging.warning(f"双击坐标转换失败: {e}")
        
        # 添加标记到图像
        self.image_view.add_marker(pos)
        
        # 如果在全屏模式下，也在全屏视图中添加标记
        if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
            fullscreen_view = self.fullscreen_window.centralWidget()
            if isinstance(fullscreen_view, ImageGraphicsView):
                fullscreen_view.add_marker(pos)
        
        # 添加记录
        self.click_records.append(record)
        self.update_record_display()
        
        # 如果时间信息未知，提示用户
        if time_info == "时间信息未知":
            QMessageBox.information(self, "提示", "该图像没有时间信息，将只记录坐标。生成报告时可能会受到限制。")
        
        # 不再记录时间标记到历史记录
    def process_batch_flatfield(self):
        """批量伪平场处理
        使用BatchFlatfieldThread进行批量伪平场处理，支持多种图像格式
        """
        # 创建对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("批量伪平场处理")
        dialog.resize(500, 300)
        
        # 创建布局
        layout = QVBoxLayout(dialog)
        
        # 输入文件夹选择
        input_layout = QHBoxLayout()
        input_label = QLabel("输入文件夹:")
        input_edit = QLineEdit()
        # 从配置文件加载上次使用的路径
        if self.config.has_section("BatchFlatfield") and self.config.has_option("BatchFlatfield", "input_folder"):
            input_edit.setText(self.config["BatchFlatfield"]["input_folder"])
        input_button = QPushButton("浏览...")
        input_layout.addWidget(input_label)
        input_layout.addWidget(input_edit)
        input_layout.addWidget(input_button)
        layout.addLayout(input_layout)
        
        # 输出文件夹选择
        output_layout = QHBoxLayout()
        output_label = QLabel("输出文件夹:")
        output_edit = QLineEdit()
        # 从配置文件加载上次使用的路径
        if self.config.has_section("BatchFlatfield") and self.config.has_option("BatchFlatfield", "output_folder"):
            output_edit.setText(self.config["BatchFlatfield"]["output_folder"])
        output_button = QPushButton("浏览...")
        output_layout.addWidget(output_label)
        output_layout.addWidget(output_edit)
        output_layout.addWidget(output_button)
        layout.addLayout(output_layout)
        
        # 添加说明文本
        info_label = QLabel("此功能会对输入文件夹中的每张图像应用伪平场处理，并将处理后的图像保存到输出文件夹中。\n"
                           "处理后的文件名将添加'FW_'前缀。\n"
                           "伪平场处理使用中值滤波算法，可以有效去除背景不均匀和噪点。")
        info_label.setWordWrap(True)
        layout.addWidget(info_label)
        
        # 添加进度指示器
        progress_layout = QHBoxLayout()
        progress_bar = QProgressBar()
        progress_bar.setVisible(False)
        progress_layout.addWidget(progress_bar)
        layout.addLayout(progress_layout)
        
        # 状态标签
        status_label = QLabel("")
        layout.addWidget(status_label)
        
        # 按钮布局
        button_layout = QHBoxLayout()
        execute_button = QPushButton("执行处理")
        execute_button.setEnabled(False)
        stop_button = QPushButton("停止处理")
        stop_button.setEnabled(False)
        cancel_button = QPushButton("取消")
        button_layout.addWidget(execute_button)
        button_layout.addWidget(stop_button)
        button_layout.addWidget(cancel_button)
        layout.addLayout(button_layout)
        
        # 检查初始路径是否有效，如果有效则激活执行按钮
        if (os.path.isdir(input_edit.text()) and 
            os.path.isdir(output_edit.text())):
            execute_button.setEnabled(True)
        
        # 设置文件夹选择按钮的事件处理器
        def browse_input_folder():
            folder = QFileDialog.getExistingDirectory(dialog, "选择输入文件夹", input_edit.text() or "")
            if folder:
                input_edit.setText(folder)
                # 保存到配置
                if not self.config.has_section("BatchFlatfield"):
                    self.config.add_section("BatchFlatfield")
                self.config["BatchFlatfield"]["input_folder"] = folder
                self.save_config()
                check_inputs()
        
        def browse_output_folder():
            folder = QFileDialog.getExistingDirectory(dialog, "选择输出文件夹", output_edit.text() or "")
            if folder:
                output_edit.setText(folder)
                # 保存到配置
                if not self.config.has_section("BatchFlatfield"):
                    self.config.add_section("BatchFlatfield")
                self.config["BatchFlatfield"]["output_folder"] = folder
                self.save_config()
                check_inputs()
        
        def check_inputs():
            execute_button.setEnabled(
                os.path.isdir(input_edit.text()) and 
                os.path.isdir(output_edit.text())
            )
        
        input_button.clicked.connect(browse_input_folder)
        output_button.clicked.connect(browse_output_folder)
        cancel_button.clicked.connect(dialog.reject)
        
        # 停止处理
        def stop_processing():
            if hasattr(self, 'flatfield_thread') and self.flatfield_thread.isRunning():
                reply = QMessageBox.question(
                    dialog,
                    "确认停止",
                    "确定要停止当前处理吗？已处理的图像将被保留。",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                
                if reply == QMessageBox.Yes:
                    self.flatfield_thread.cancel()
                    status_label.setText("正在停止处理...")
                    stop_button.setEnabled(False)
        
        stop_button.clicked.connect(stop_processing)
        
        # 执行批量伪平场处理
        def execute_flatfield():
            try:
                # 获取文件夹路径
                input_folder = input_edit.text()
                output_folder = output_edit.text()
                
                # 获取输入文件夹中的图像文件
                image_files = [f for f in os.listdir(input_folder) 
                              if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff', '.fits', '.fit', '.fts'))]
                
                if not image_files:
                    QMessageBox.warning(dialog, "警告", "输入文件夹中没有找到支持的图像文件")
                    return
                
                # 更新UI
                status_label.setText("开始处理图像...")
                progress_bar.setVisible(True)
                progress_bar.setMaximum(len(image_files))
                progress_bar.setValue(0)
                execute_button.setEnabled(False)
                stop_button.setEnabled(True)
                QApplication.processEvents()
                
                # 创建并启动工作线程
                self.flatfield_thread = BatchFlatfieldThread(input_folder, output_folder, image_files)
                self.flatfield_thread.progress_signal.connect(lambda idx, file: update_progress(idx, file, len(image_files)))
                self.flatfield_thread.complete_signal.connect(lambda: on_flatfield_complete())
                self.flatfield_thread.error_signal.connect(lambda err: on_flatfield_error(err))
                self.flatfield_thread.start()
            
            except Exception as e:
                QMessageBox.critical(dialog, "错误", f"启动处理过程时出错: {str(e)}")
                execute_button.setEnabled(True)
                stop_button.setEnabled(False)
        
        def update_progress(idx, file, total):
            progress_bar.setValue(idx + 1)
            status_label.setText(f"正在处理: {file} ({idx + 1}/{total})")
            QApplication.processEvents()
        
        def on_flatfield_complete():
            status_label.setText("处理完成!")
            QMessageBox.information(dialog, "完成", "批量伪平场处理完成!")
            execute_button.setEnabled(True)
            stop_button.setEnabled(False)
        
        def on_flatfield_error(error):
            QMessageBox.critical(dialog, "错误", f"处理过程中出错: {error}")
            execute_button.setEnabled(True)
            stop_button.setEnabled(False)
        
        execute_button.clicked.connect(execute_flatfield)
        
        # 显示对话框
        dialog.exec_()
        
    def process_flatfield(self):
        """
        对当前图像应用伪平场处理并保存
        
        使用简单但高效的中值滤波算法，无需任何参数设置
        同时进行噪点去除处理，提供更干净的图像输出
        """
        # 确保有图像在显示
        if not self.images:
            QMessageBox.warning(self, "警告", "请先导入图像")
            return
        
        # 获取当前图像路径
        current_image = self.images[self.current_index]
        if isinstance(current_image, dict):
            current_path = current_image['filename']
        else:
            current_path = current_image
        
        # 创建进度对话框
        progress = QProgressDialog("正在应用伪平场处理与噪点去除...", "取消", 0, 100, self)
        progress.setWindowTitle("处理中")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        try:
            # 确定输出文件名 - 使用 FW_ 前缀
            file_basename = os.path.basename(current_path)
            file_dir = os.path.dirname(current_path)
            output_name = f"FW_{file_basename}"
            output_path = os.path.join(file_dir, output_name)
            
            progress.setValue(10)
            if progress.wasCanceled():
                return
            
            # 创建并启动工作线程
            self.flatfield_thread = SingleFlatfieldThread(current_path, file_dir)
            
            # 连接信号
            def on_complete(output_path):
                # 更新当前显示的图像
                if isinstance(self.images[self.current_index], dict):
                    self.images[self.current_index]['filename'] = output_path
                    # 重新加载图像
                    self.show_current_image()
                else:
                    self.images[self.current_index] = output_path
                    self.show_current_image()
                
                progress.setValue(100)
                progress.close()
                QMessageBox.information(self, "完成", f"伪平场处理完成，结果已保存到:\n{output_path}")
            
            def on_error(error_msg):
                progress.close()
                QMessageBox.critical(self, "错误", f"单张伪平场处理失败: {error_msg}")
            
            # 连接信号
            self.flatfield_thread.complete_signal.connect(on_complete)
            self.flatfield_thread.error_signal.connect(on_error)
            
            # 启动线程
            self.flatfield_thread.start()
            
        except Exception as e:
            progress.close()
            QMessageBox.critical(self, "错误", f"伪平场处理失败: {str(e)}")
    
    def batch_align_images(self):
        """批量对齐图像
        使用BatchAlignThread进行批量图像对齐，支持多种图像格式
        """
        # 打开设置对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("批量图像对齐")
        dialog.setMinimumWidth(500)
        
        layout = QVBoxLayout(dialog)
        
        # 创建表单布局
        form_layout = QFormLayout()
        
        # 选择新图像文件夹
        new_folder_layout = QHBoxLayout()
        new_folder_edit = QLineEdit()
        new_folder_edit.setReadOnly(True)
        # 从配置文件加载上次使用的路径
        if self.config.has_section("BatchAlignment") and self.config.has_option("BatchAlignment", "new_folder"):
            new_folder_edit.setText(self.config["BatchAlignment"]["new_folder"])
        new_folder_button = QPushButton("浏览...")
        new_folder_layout.addWidget(new_folder_edit)
        new_folder_layout.addWidget(new_folder_button)
        form_layout.addRow("新图像文件夹:", new_folder_layout)
        
        # 选择参考图像文件夹
        old_folder_layout = QHBoxLayout()
        old_folder_edit = QLineEdit()
        old_folder_edit.setReadOnly(True)
        # 从配置文件加载上次使用的路径
        if self.config.has_section("BatchAlignment") and self.config.has_option("BatchAlignment", "old_folder"):
            old_folder_edit.setText(self.config["BatchAlignment"]["old_folder"])
        old_folder_button = QPushButton("浏览...")
        old_folder_layout.addWidget(old_folder_edit)
        old_folder_layout.addWidget(old_folder_button)
        form_layout.addRow("参考图像文件夹:", old_folder_layout)
        
        # 选择输出文件夹
        output_folder_layout = QHBoxLayout()
        output_folder_edit = QLineEdit()
        output_folder_edit.setReadOnly(True)
        # 从配置文件加载上次使用的路径
        if self.config.has_section("BatchAlignment") and self.config.has_option("BatchAlignment", "output_folder"):
            output_folder_edit.setText(self.config["BatchAlignment"]["output_folder"])
        output_folder_button = QPushButton("浏览...")
        output_folder_layout.addWidget(output_folder_edit)
        output_folder_layout.addWidget(output_folder_button)
        form_layout.addRow("输出文件夹:", output_folder_layout)
        
        # 添加表单布局
        layout.addLayout(form_layout)
        
        # 添加说明文本
        info_label = QLabel("此功能会将新图像文件夹中的每张图像与参考图像文件夹中的对应图像进行对齐，"
                           "并将对齐后的图像保存到输出文件夹中。\n"
                           "对齐算法使用astroalign库，适用于天文图像的自动对齐。\n"
                           "注意：图像匹配基于文件名，请确保新图像和参考图像有相似的文件名。")
        info_label.setWordWrap(True)
        layout.addWidget(info_label)
        
        # 添加进度指示器
        progress_layout = QHBoxLayout()
        progress_bar = QProgressBar()
        progress_bar.setVisible(False)
        progress_layout.addWidget(progress_bar)
        layout.addLayout(progress_layout)
        
        # 状态标签
        status_label = QLabel("")
        layout.addWidget(status_label)
        
        # 按钮布局
        button_layout = QHBoxLayout()
        execute_button = QPushButton("执行对齐")
        execute_button.setEnabled(False)
        stop_button = QPushButton("停止处理")
        stop_button.setEnabled(False)
        cancel_button = QPushButton("取消")
        button_layout.addWidget(execute_button)
        button_layout.addWidget(stop_button)
        button_layout.addWidget(cancel_button)
        layout.addLayout(button_layout)
        
        # 检查初始路径是否有效，如果有效则激活执行按钮
        if (os.path.isdir(new_folder_edit.text()) and 
            os.path.isdir(old_folder_edit.text()) and 
            os.path.isdir(output_folder_edit.text())):
            execute_button.setEnabled(True)
        
        # 设置文件夹选择按钮的事件处理器
        def browse_new_folder():
            folder = QFileDialog.getExistingDirectory(dialog, "选择新图像文件夹", new_folder_edit.text() or "")
            if folder:
                new_folder_edit.setText(folder)
                # 保存到配置
                if not self.config.has_section("BatchAlignment"):
                    self.config.add_section("BatchAlignment")
                self.config["BatchAlignment"]["new_folder"] = folder
                self.save_config()
                check_inputs()
        
        def browse_old_folder():
            folder = QFileDialog.getExistingDirectory(dialog, "选择参考图像文件夹", old_folder_edit.text() or "")
            if folder:
                old_folder_edit.setText(folder)
                # 保存到配置
                if not self.config.has_section("BatchAlignment"):
                    self.config.add_section("BatchAlignment")
                self.config["BatchAlignment"]["old_folder"] = folder
                self.save_config()
                check_inputs()
        
        def browse_output_folder():
            folder = QFileDialog.getExistingDirectory(dialog, "选择输出文件夹", output_folder_edit.text() or "")
            if folder:
                output_folder_edit.setText(folder)
                # 保存到配置
                if not self.config.has_section("BatchAlignment"):
                    self.config.add_section("BatchAlignment")
                self.config["BatchAlignment"]["output_folder"] = folder
                self.save_config()
                check_inputs()
        
        def check_inputs():
            execute_button.setEnabled(
                os.path.isdir(new_folder_edit.text()) and 
                os.path.isdir(old_folder_edit.text()) and 
                os.path.isdir(output_folder_edit.text())
            )
        
        new_folder_button.clicked.connect(browse_new_folder)
        old_folder_button.clicked.connect(browse_old_folder)
        output_folder_button.clicked.connect(browse_output_folder)
        cancel_button.clicked.connect(dialog.reject)
        
        # 停止处理
        def stop_processing():
            if hasattr(self, 'align_thread') and self.align_thread.isRunning():
                reply = QMessageBox.question(
                    dialog,
                    "确认停止",
                    "确定要停止当前对齐处理吗？已处理的图像将被保留。",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                
                if reply == QMessageBox.Yes:
                    self.align_thread.cancel()
                    status_label.setText("正在停止处理...")
                    stop_button.setEnabled(False)
        
        stop_button.clicked.connect(stop_processing)
        
        # 执行对齐操作
        def execute_alignment():
            try:
                # 获取文件夹路径
                new_folder = new_folder_edit.text()
                old_folder = old_folder_edit.text()
                aligned_folder = output_folder_edit.text()
                aligned_new_folder = os.path.join(aligned_folder, 'new')
                
                # 创建保存对齐后图像的文件夹
                if not os.path.exists(aligned_folder):
                    os.makedirs(aligned_folder)
                if not os.path.exists(aligned_new_folder):
                    os.makedirs(aligned_new_folder)
                
                # 获取两个文件夹中的图像文件名
                new_files = sorted([f for f in os.listdir(new_folder) if f.lower().endswith(('.fit', '.fits', '.fts'))])
                old_files = sorted([f for f in os.listdir(old_folder) if f.lower().endswith(('.fit', '.fits', '.fts'))])
                
                # 更新UI
                status_label.setText("开始对齐图像...")
                progress_bar.setVisible(True)
                progress_bar.setMaximum(len(new_files))
                progress_bar.setValue(0)
                execute_button.setEnabled(False)
                stop_button.setEnabled(True)
                QApplication.processEvents()
                
                # 创建并启动工作线程
                self.align_thread = BatchAlignThread(new_folder, old_folder, aligned_new_folder, new_files, old_files)
                self.align_thread.progress_signal.connect(lambda idx, file, len_new=len(new_files): update_progress(idx, file, len_new))
                self.align_thread.complete_signal.connect(lambda: on_alignment_complete())
                self.align_thread.error_signal.connect(lambda err: on_alignment_error(err))
                self.align_thread.start()
            
            except Exception as e:
                QMessageBox.critical(dialog, "错误", f"启动对齐过程时出错: {str(e)}")
                execute_button.setEnabled(True)
                stop_button.setEnabled(False)
        
        def update_progress(idx, file, total):
            progress_bar.setValue(idx + 1)
            status_label.setText(f"正在对齐: {file} ({idx + 1}/{total})")
            QApplication.processEvents()
        
        def on_alignment_complete():
            status_label.setText("对齐完成!")
            QMessageBox.information(dialog, "完成", "图像对齐完成!")
            execute_button.setEnabled(True)
            stop_button.setEnabled(False)
        
        def on_alignment_error(error):
            QMessageBox.critical(dialog, "错误", f"对齐过程中出错: {error}")
            execute_button.setEnabled(True)
            stop_button.setEnabled(False)
        
        execute_button.clicked.connect(execute_alignment)
        
        # 显示对话框
        dialog.exec_()
    def toggle_drawing_line(self):
        """切换辅助线绘制模式"""
        try:
            if self.image_view:
                # 检查是否正在播放动画
                if self.is_playing:
                    # 如果正在播放动画，先暂停动画再开启辅助线绘制
                    self.toggle_play()  # 暂停播放
                    QMessageBox.information(self, "提示", "已暂停播放以启用辅助线绘制模式")
                    self.draw_line_action.setChecked(True)
                    self.image_view.start_drawing_line()
                else:
                    if self.draw_line_action.isChecked():
                        self.image_view.start_drawing_line()
                    else:
                        self.image_view.stop_drawing_line()
        except Exception as e:
            print(f"切换辅助线绘制模式时出错: {e}")
            # 确保按钮状态正确
            self.draw_line_action.setChecked(False)
    
    def remove_last_auxiliary_line(self):
        """删除最后一条辅助线"""
        if self.image_view:
            self.image_view.remove_auxiliary_line()
            # 移除状态栏消息
            # self.statusBar().showMessage("已删除最后一条辅助线")
    
    def clear_all_auxiliary_lines(self):
        """清除所有辅助线"""
        if self.image_view:
            self.image_view.clear_auxiliary_lines()
            # 移除状态栏消息
            # self.statusBar().showMessage("已清除所有辅助线")
    
    def apply_color_filter(self, color):
        """应用颜色滤镜"""
        if self.image_view:
            self.color_filter = color  # 保存当前的颜色滤镜设置
            self.image_view.set_color_filter(color)
            
            # 如果在全屏模式下，也应用到全屏视图
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                fullscreen_view = self.fullscreen_window.centralWidget()
                if fullscreen_view:
                    fullscreen_view.set_color_filter(color)
                    
            # 移除状态栏消息
            # if color:
            #     self.statusBar().showMessage(f"已应用{color}色滤镜")
            # else:
            #     self.statusBar().showMessage("已恢复原色")
                    
            # 如果在叠加模式，并且已经有叠加图像，重新创建叠加图像并应用滤镜
            if self.is_stacked and self.stacked_image is not None:
                # 重置叠加图像
                self.stacked_image = None
                # 重新创建叠加图像
                self.create_stacked_image()
                # 如果创建成功，重新显示
                if self.stacked_image is not None:
                    # 保存当前缩放因子
                    current_zoom = self.zoom_slider.value()
                    self.image_view.set_image(self.stacked_image, filename="叠加图像")
                    # 应用之前的缩放因子
                    self.update_zoom(current_zoom)
    
    def toggle_auxiliary_lines(self):
        """切换辅助线显示状态"""
        try:
            if not self.image_view:
                return
                
            # 获取当前辅助线可见性
            visible = self.image_view.are_auxiliary_lines_visible()
            
            # 切换可见性
            self.image_view.set_auxiliary_lines_visible(not visible)
            
            # 保存辅助线可见性状态到全局设置，确保对所有图片生效
            settings = QSettings("CCOR", "ImageViewer")
            settings.setValue("auxiliary_lines_visible", not visible)
            settings.sync()
            
            if not visible:
                # 移除状态栏消息
                # self.statusBar().showMessage("已显示辅助线（对所有图片生效）")
                self.hide_lines_action.setText("隐藏辅助线")
                self.hide_lines_action.setChecked(True)
            else:
                # 移除状态栏消息
                # self.statusBar().showMessage("已隐藏辅助线（对所有图片生效）")
                self.hide_lines_action.setText("显示辅助线")
                self.hide_lines_action.setChecked(False)
        except Exception as e:
            print(f"切换辅助线显示状态时出错: {e}")
            # 移除状态栏消息
            # self.statusBar().showMessage(f"切换辅助线显示状态失败: {e}")
    
    def toggle_stack_mode(self):
        """切换图像叠加模式"""
        if not self.images:
            QMessageBox.warning(self, "警告", "请先加载图像！")
            self.stack_action.setChecked(False)
            return
            
        # 切换叠加状态
        self.is_stacked = not self.is_stacked
        
        if self.is_stacked:
            # 进入叠加模式前先显示设置对话框
            show_settings = self.show_stack_settings()
            
            if not show_settings:
                # 用户取消了设置，回到普通模式
                self.is_stacked = False
                self.stack_action.setChecked(False)
                return
            
            # 创建叠加图像
            self.stacked_image = self.create_stacked_image()
                
            # 显示叠加后的图像
            if self.stacked_image is not None:
                # 保存当前缩放因子
                current_zoom = self.zoom_slider.value()
                # 显示叠加图像，并在文件名中包含设置信息
                stack_info = f"叠加图像 [亮度:{self.brightness_value}% 对比度:{self.contrast_value}%]"
                self.image_view.set_image(self.stacked_image, filename=stack_info)
                # 应用之前的缩放因子
                self.update_zoom(current_zoom)
                
                # 添加到历史记录
                history_record = self.history_logger.add_record(
                    "图像叠加", 
                    f"开启图像叠加模式，叠加了 {len(self.images)} 张图像，亮度:{self.brightness_value}%，对比度:{self.contrast_value}%"
                )
                self.update_history_display(history_record)
                
                # 禁用一部分按钮
                self.prev_image_action.setEnabled(False)
                self.next_image_action.setEnabled(False)
                self.play_forward_action.setEnabled(False)
                self.play_backward_action.setEnabled(False)
                self.pause_action.setEnabled(False)
                # 禁用翻转按钮，因为翻转只适用于单独图像
                self.horizontal_flip_action.setEnabled(False)
                self.vertical_flip_action.setEnabled(False)
                
                # 启用叠加设置按钮
                self.stack_settings_action.setEnabled(True)
            else:
                # 叠加失败
                self.is_stacked = False
                self.stack_action.setChecked(False)
                QMessageBox.warning(self, "警告", "图像叠加失败！请检查图像是否兼容。")
        else:
            # 退出叠加模式，恢复显示当前图像
            self.show_current_image()
            
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "图像叠加", 
                f"关闭图像叠加模式"
            )
            self.update_history_display(history_record)
            
            # 恢复按钮状态
            self.enable_buttons()
            
            # 禁用叠加设置按钮
            self.stack_settings_action.setEnabled(False)
    
    def show_stack_settings(self):
        """显示叠加设置对话框"""
        dialog = QDialog(self)
        dialog.setWindowTitle("叠加设置")
        dialog.setFixedSize(400, 450)  # 增加高度以适应报告输入区域
        
        layout = QVBoxLayout()
        dialog.setLayout(layout)
        
        # 亮度调整
        layout.addWidget(QLabel("- 亮度调整 -"))
        layout.addWidget(QLabel("提示: 降低亮度（<100%）可以更好地观察彗星尾迹"))
        
        brightness_layout = QHBoxLayout()
        brightness_layout.addWidget(QLabel("亮度:"))
        
        brightness_value_label = QLabel(f"{self.brightness_value}%")
        
        brightness_slider = QSlider(Qt.Horizontal)
        brightness_slider.setRange(10, 150)  # 10% - 150%，扩大范围
        brightness_slider.setValue(self.brightness_value)
        brightness_slider.valueChanged.connect(lambda value: self.update_brightness_value(value, brightness_value_label))
        
        brightness_layout.addWidget(brightness_slider)
        brightness_layout.addWidget(brightness_value_label)
        layout.addLayout(brightness_layout)
        
        # 对比度调整
        layout.addWidget(QLabel("- 对比度阈值调整 -"))
        layout.addWidget(QLabel("注: 数值表示最亮的百分之几的像素将被强调显示"))
        
        contrast_layout = QHBoxLayout()
        contrast_layout.addWidget(QLabel("阈值:"))
        
        contrast_value_label = QLabel(f"{self.contrast_value}%")
        
        contrast_slider = QSlider(Qt.Horizontal)
        contrast_slider.setRange(1, 20)  # 1% - 20%
        contrast_slider.setValue(self.contrast_value)
        contrast_slider.valueChanged.connect(lambda value: self.update_contrast_value(value, contrast_value_label))
        
        contrast_layout.addWidget(contrast_slider)
        contrast_layout.addWidget(contrast_value_label)
        layout.addLayout(contrast_layout)
        
        # 添加分隔线
        layout.addWidget(QFrame(frameShape=QFrame.HLine, frameShadow=QFrame.Sunken))
        
        # 叠加方式选择
        stacking_mode_layout = QHBoxLayout()
        stacking_mode_label = QLabel("叠加方式:")
        stacking_mode_layout.addWidget(stacking_mode_label)
        
        stacking_mode_combo = QComboBox()
        stacking_mode_combo.addItem("最大值叠加 (适合寻找彗星)", "maximum")
        stacking_mode_combo.addItem("平均值叠加", "average")
        stacking_mode_combo.addItem("按报告叠加 (彗星轨迹对齐)", "report")
        # 设置当前选中的叠加模式
        if self.stacking_mode == "average":
            stacking_mode_combo.setCurrentIndex(1)
        else:
            stacking_mode_combo.setCurrentIndex(0)
            
        stacking_mode_combo.currentIndexChanged.connect(lambda idx: self.update_stacking_mode(stacking_mode_combo.itemData(idx) or stacking_mode_combo.currentText().lower()))
        
        stacking_mode_layout.addWidget(stacking_mode_combo)
        layout.addLayout(stacking_mode_layout)
        
        # 添加分隔线
        layout.addWidget(QFrame(frameShape=QFrame.HLine, frameShadow=QFrame.Sunken))
        
        # 伽马值调整
        gamma_layout = QHBoxLayout()
        gamma_layout.addWidget(QLabel("伽马值:"))
        
        gamma_value_label = QLabel(f"{self.gamma_value:.1f}")
        
        gamma_slider = QSlider(Qt.Horizontal)
        gamma_slider.setRange(5, 20)  # 0.5 - 2.0
        gamma_slider.setValue(int(self.gamma_value * 10))
        gamma_slider.valueChanged.connect(lambda value: self.update_gamma_value(value, gamma_value_label))
        
        gamma_layout.addWidget(gamma_slider)
        gamma_layout.addWidget(gamma_value_label)
        layout.addLayout(gamma_layout)
        
        # 添加反色（负片）选项
        negative_checkbox = QCheckBox("使用反色效果（增强暗区域）")
        negative_checkbox.setChecked(self.use_negative)
        negative_checkbox.toggled.connect(self.toggle_negative)
        layout.addWidget(negative_checkbox)
        
        # 高性能模式
        high_performance_checkbox = QCheckBox("高性能模式（处理速度更快）")
        high_performance_checkbox.setChecked(self.high_performance)
        high_performance_checkbox.toggled.connect(self.toggle_high_performance)
        layout.addWidget(high_performance_checkbox)
        
        # 报告输入区域（仅在选择按报告叠加模式时显示）
        self.report_input_group = QGroupBox("彗星报告数据")
        self.report_input_group.setVisible(self.stacking_mode == "report")
        report_layout = QVBoxLayout()
        
        report_label = QLabel("请输入彗星报告数据，格式为：HH:MM x y，例如 20:30 426 1057")
        report_layout.addWidget(report_label)
        
        self.report_text_edit = QTextEdit()
        self.report_text_edit.setPlaceholderText("20:30 426 1057\n20:45 437 1054\n21:00 449 1052\n21:15 459 1050")
        self.report_text_edit.setMaximumHeight(100)
        report_layout.addWidget(self.report_text_edit)
        
        # 如果已经有报告数据，则显示
        if hasattr(self, 'report_data_text'):
            self.report_text_edit.setText(self.report_data_text)
        
        self.report_input_group.setLayout(report_layout)
        layout.addWidget(self.report_input_group)
        
        # 按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(lambda: self.save_report_data_and_close(dialog))
        button_box.rejected.connect(dialog.reject)
        layout.addWidget(button_box)
        
        # 显示对话框并返回结果
        return dialog.exec_() == QDialog.Accepted
        
    def save_report_data_and_close(self, dialog):
        """保存报告数据并关闭对话框"""
        # 如果是报告叠加模式，保存报告数据
        if self.stacking_mode == "report" and hasattr(self, 'report_text_edit'):
            self.report_data_text = self.report_text_edit.toPlainText()
        
        # 关闭对话框
        dialog.accept()
        
    def update_brightness_value(self, value, label):
        """更新亮度值"""
        self.brightness_value = value
        label.setText(f"{value}%")
        
        # 保存设置
        self.save_config()
        
    def update_contrast_value(self, value, label):
        """更新对比度值"""
        self.contrast_value = value
        label.setText(f"{value}%")
        
        # 保存设置
        self.save_config()
        
    def update_stacking_mode(self, mode):
        """更新叠加模式"""
        self.stacking_mode = mode
        
        # 如果在叠加设置对话框中，更新报告输入区域的可见性
        if hasattr(self, 'report_input_group'):
            self.report_input_group.setVisible(mode == "report")
        
        # 保存设置
        self.save_config()
        
    def parse_report_data(self, report_text):
        """解析报告文本，提取时间和坐标信息"""
        try:
            # 保存报告文本，以便在再次打开设置对话框时显示
            self.report_data_text = report_text
            
            # 解析报告文本
            lines = [line.strip() for line in report_text.replace('\r', '').splitlines() if line.strip()]
            coord_data = []
            
            for line in lines:
                parts = line.strip().split()
                if len(parts) >= 3:
                    try:
                        # 预处理时间格式
                        raw_time = parts[0]
                        if ':' not in raw_time:
                            if len(raw_time) == 4:
                                raw_time = f"{raw_time[:2]}:{raw_time[2:]}"
                            elif len(raw_time) == 6:
                                raw_time = f"{raw_time[:2]}:{raw_time[2:4]}:{raw_time[4:]}"
                        
                        time_str = self.convert_time_format(raw_time)
                        x = int(parts[1])
                        y = int(parts[2])
                        coord_data.append({'time': time_str, 'x': x, 'y': y})
                    except (ValueError, IndexError) as e:
                        print(f"解析行 '{line}' 时出错: {e}")
            
            return coord_data
        except Exception as e:
            print(f"解析报告数据时出错: {e}")
            return []
    
    def create_stacked_image(self):
        """创建叠加图像"""
        try:
            # 检查是否有足够的图像
            if len(self.images) < 2 or len(self.original_images) < 2:
                return None
                
            # 获取当前设置
            brightness_factor = self.brightness_value / 100.0
            
            # 优化亮度调整，让低亮度值效果更明显
            if brightness_factor < 1.0:
                # 使用更强的指数效应，使低亮度设置更有效
                brightness_factor = brightness_factor ** 2.5
                
            contrast_percentile = self.contrast_value
            
            # 显示进度对话框
            progress = QProgressDialog("创建叠加图像中...", "取消", 0, len(self.images), self)
            progress.setWindowTitle("处理中")
            progress.setWindowModality(Qt.WindowModal)
            progress.show()
            
            # 检查至少一张图像的维度
            for img_data in self.original_images:
                if img_data is not None and len(img_data.shape) >= 2:
                    height, width = img_data.shape[:2]
                    break
            else:
                QMessageBox.warning(self, "警告", "无法获取图像尺寸！")
                return None
            
            # 叠加图像
            if self.stacking_mode == "report":
                # 按报告叠加（彗星轨迹对齐）
                # 检查是否有报告数据
                if not hasattr(self, 'report_data_text') or not self.report_data_text:
                    QMessageBox.warning(self, "警告", "没有彗星报告数据！请在叠加设置中输入报告数据。")
                    return None
                
                # 解析报告数据
                coord_data = self.parse_report_data(self.report_data_text)
                if not coord_data or len(coord_data) < 2:
                    QMessageBox.warning(self, "警告", "未找到足够的坐标数据！至少需要两个时间点的坐标。")
                    return None
                
                # 导入scipy的shift函数用于图像偏移
                from scipy import ndimage
                
                # 计算参考点（使用第一个坐标作为参考）
                ref_x, ref_y = coord_data[0]['x'], coord_data[0]['y']
                
                # 计算彗星移动速率
                # 根据报告中的时间和坐标计算彗星的移动速率
                if len(coord_data) >= 2:
                    # 计算时间差（分钟）
                    time_diffs = []
                    x_diffs = []
                    y_diffs = []
                    
                    for i in range(1, len(coord_data)):
                        prev_time = coord_data[i-1]['time'].split(':')
                        curr_time = coord_data[i]['time'].split(':')
                        
                        prev_minutes = int(prev_time[0]) * 60 + int(prev_time[1])
                        curr_minutes = int(curr_time[0]) * 60 + int(curr_time[1])
                        time_diff = curr_minutes - prev_minutes
                        
                        if time_diff > 0:
                            time_diffs.append(time_diff)
                            x_diffs.append(coord_data[i]['x'] - coord_data[i-1]['x'])
                            y_diffs.append(coord_data[i]['y'] - coord_data[i-1]['y'])
                    
                    # 计算平均速率（像素/分钟）
                    if time_diffs:
                        avg_x_rate = sum(x_diffs) / sum(time_diffs)  # x方向速率
                        avg_y_rate = sum(y_diffs) / sum(time_diffs)  # y方向速率
                    else:
                        avg_x_rate = avg_y_rate = 0
                else:
                    avg_x_rate = avg_y_rate = 0
                
                # 获取报告中的第一个时间点作为参考时间
                ref_time_parts = coord_data[0]['time'].split(':')
                ref_minutes = int(ref_time_parts[0]) * 60 + int(ref_time_parts[1])
                
                # 计算所有图像的偏移量，以确定最终画布的大小
                max_shift_x_pos = 0  # 最大正向X偏移
                max_shift_x_neg = 0  # 最大负向X偏移
                max_shift_y_pos = 0  # 最大正向Y偏移
                max_shift_y_neg = 0  # 最大负向Y偏移
                
                # 首先计算所有图像的偏移量，找出最大偏移
                for i, image in enumerate(self.images):
                    filename = os.path.basename(image['filename'])
                    time_match = re.search(r'T[^\d]*(\d{2})[^\d]*(\d{2})', filename)
                    if time_match:
                        hour = time_match.group(1)
                        minute = time_match.group(2)
                        img_minutes = int(hour) * 60 + int(minute)
                        time_diff = img_minutes - ref_minutes
                        shift_x = -int(time_diff * avg_x_rate)
                        shift_y = -int(time_diff * avg_y_rate)
                        
                        # 更新最大偏移值
                        if shift_x > 0:
                            max_shift_x_pos = max(max_shift_x_pos, shift_x)
                        else:
                            max_shift_x_neg = max(max_shift_x_neg, -shift_x)
                        
                        if shift_y > 0:
                            max_shift_y_pos = max(max_shift_y_pos, shift_y)
                        else:
                            max_shift_y_neg = max(max_shift_y_neg, -shift_y)
                
                # 创建足够大的画布，以容纳所有偏移后的图像
                new_width = width + max_shift_x_pos + max_shift_x_neg
                new_height = height + max_shift_y_pos + max_shift_y_neg
                
                # 创建结果图像，使用新的尺寸
                stacked = np.zeros((new_height, new_width), dtype=np.float32)
                count = 0
                
                # 计算原始图像在新画布中的偏移位置
                base_offset_x = max_shift_x_neg
                base_offset_y = max_shift_y_neg
                
                # 叠加所有图像，不再根据时间匹配，而是对所有图像应用偏移
                for i, (img_data, image) in enumerate(zip(self.original_images, self.images)):
                    if progress.wasCanceled():
                        return None
                    
                    if img_data.shape != (height, width):
                        progress.setValue(i + 1)
                        continue
                    
                    # 获取图像文件名
                    filename = os.path.basename(image['filename'])
                    
                    # 从文件名中提取时间信息
                    time_match = re.search(r'T[^\d]*(\d{2})[^\d]*(\d{2})', filename)
                    if time_match:
                        hour = time_match.group(1)
                        minute = time_match.group(2)
                        
                        # 计算当前图像时间（分钟）
                        img_minutes = int(hour) * 60 + int(minute)
                        
                        # 计算时间差（相对于参考时间）
                        time_diff = img_minutes - ref_minutes
                        
                        # 根据时间差和速率计算偏移量
                        # 为了对齐彗星，我们需要向相反方向偏移图像
                        shift_x = -int(time_diff * avg_x_rate)
                        shift_y = -int(time_diff * avg_y_rate)
                        
                        # 在新画布上放置图像
                        # 计算图像在新画布中的位置
                        pos_x = base_offset_x + shift_x
                        pos_y = base_offset_y + shift_y
                        
                        # 确保位置不超出边界
                        if pos_x >= 0 and pos_y >= 0 and pos_x + width <= new_width and pos_y + height <= new_height:
                            # 直接放置图像到指定位置
                            stacked[pos_y:pos_y+height, pos_x:pos_x+width] = np.maximum(
                                stacked[pos_y:pos_y+height, pos_x:pos_x+width],
                                img_data.astype(np.float32)
                            )
                        else:
                            # 如果位置超出边界，使用shift函数进行偏移
                            # 创建临时画布
                            temp_canvas = np.zeros((new_height, new_width), dtype=np.float32)
                            # 将原图放在基准位置
                            temp_canvas[base_offset_y:base_offset_y+height, base_offset_x:base_offset_x+width] = img_data.astype(np.float32)
                            # 应用偏移
                            shifted_img = ndimage.shift(temp_canvas, (shift_y, shift_x), mode='constant', cval=0)
                            # 叠加到结果图像
                            stacked = np.maximum(stacked, shifted_img)
                        count += 1
                    else:
                        # 如果无法从文件名提取时间，仍然添加到叠加中但不偏移
                        # 直接放置在基准位置
                        stacked[base_offset_y:base_offset_y+height, base_offset_x:base_offset_x+width] = np.maximum(
                            stacked[base_offset_y:base_offset_y+height, base_offset_x:base_offset_x+width],
                            img_data.astype(np.float32)
                        )
                        count += 1
                    
                    progress.setValue(i + 1)
                
                # 检查是否成功叠加了图像
                if count == 0:
                    QMessageBox.warning(self, "警告", "没有有效的图像可以叠加！")
                    return None
                    
            elif self.stacking_mode == "maximum":
                # 最大值叠加
                stacked = np.zeros((height, width), dtype=np.float32)
                
                # 叠加所有图像
                for i, img_data in enumerate(self.original_images):
                    if progress.wasCanceled():
                        return None
                    
                    if img_data.shape == (height, width):
                        # 使用最大值叠加方式，适合寻找彗星
                        stacked = np.maximum(stacked, img_data.astype(np.float32))
                    progress.setValue(i + 1)
            else:
                # 平均值叠加
                stacked = np.zeros((height, width), dtype=np.float32)
                count = 0
                
                # 叠加所有图像
                for i, img_data in enumerate(self.original_images):
                    if progress.wasCanceled():
                        return None
                    
                    if img_data.shape == (height, width):
                        stacked += img_data.astype(np.float32)
                        count += 1
                    progress.setValue(i + 1)
                
                # 计算平均值
                if count > 0:
                    stacked /= count
            
            # 应用亮度调整
            stacked = stacked * brightness_factor
                
            # 应用伽马校正
            if self.gamma_value != 1.0:
                # 伽马校正
                epsilon = 1e-5  # 避免对零进行幂操作
                stacked = np.power((stacked + epsilon) / 255.0, self.gamma_value) * 255.0
            
            # 增强对比度
            if contrast_percentile > 0:
                # 找出亮度超过指定百分位数的像素
                flat_stacked = stacked.flatten()
                threshold = np.percentile(flat_stacked, 100 - contrast_percentile)
                
                # 应用阈值处理，增强亮区域
                mask = stacked > threshold
                stacked[mask] = 255
            
            # 应用对数变换增强极暗区域（尤其当亮度较低时）
            if self.brightness_value < 50:
                epsilon = 1e-5  # 避免对零取对数
                log_factor = 255.0 / np.log1p(255 + epsilon)
                stacked = np.log1p(stacked + epsilon) * log_factor
            
            # 控制范围在0-255
            stacked = np.clip(stacked, 0, 255)
            
            # 应用反色（负片）效果
            if self.use_negative:
                stacked = 255 - stacked
            
            # 转换为8位格式
            stacked_8bit = stacked.astype(np.uint8)
            
            # 转换为QImage
            height, width = stacked_8bit.shape
            qimage = QImage(stacked_8bit.data, width, height, width, QImage.Format_Grayscale8)
            
            # 返回QPixmap
            return QPixmap.fromImage(qimage)
        
        except Exception as e:
            QMessageBox.critical(self, "错误", f"创建叠加图像时出错: {e}")
            import traceback
            traceback.print_exc()
            return None
    
    def show_line_settings(self):
        """显示辅助线设置对话框"""
        try:
            dialog = QDialog(self)
            dialog.setWindowTitle("辅助线设置")
            dialog.setFixedSize(350, 250)
            
            layout = QVBoxLayout(dialog)
            
            # 辅助线操作按钮
            layout.addWidget(QLabel("辅助线操作:"))
            
            # 删除最后一条辅助线
            remove_last_btn = QPushButton("删除最后一条辅助线")
            remove_last_btn.clicked.connect(self.remove_last_auxiliary_line)
            # 不再自动关闭窗口
            layout.addWidget(remove_last_btn)
            
            # 删除所有辅助线
            clear_all_btn = QPushButton("删除所有辅助线")
            clear_all_btn.clicked.connect(self.clear_all_auxiliary_lines)
            # 不再自动关闭窗口
            layout.addWidget(clear_all_btn)
            
            # 导入导出辅助线
            io_layout = QHBoxLayout()
            
            # 导出辅助线
            export_btn = QPushButton("导出辅助线")
            export_btn.clicked.connect(self.export_auxiliary_lines)
            io_layout.addWidget(export_btn)
            
            # 导入辅助线
            import_btn = QPushButton("导入辅助线")
            import_btn.clicked.connect(self.import_auxiliary_lines)
            io_layout.addWidget(import_btn)
            
            layout.addLayout(io_layout)
            
            # 关闭按钮
            close_btn = QPushButton("关闭")
            close_btn.clicked.connect(dialog.reject)
            layout.addWidget(close_btn)
            
            # 显示对话框
            dialog.exec_()
        except Exception as e:
            print(f"显示辅助线设置对话框时出错: {e}")
    
    def export_auxiliary_lines(self):
        """导出辅助线到JSON文件"""
        if not self.image_view or not self.image_view.auxiliary_lines:
            QMessageBox.information(self, "导出辅助线", "当前没有辅助线可以导出")
            return
        
        # 打开文件对话框
        file_path, _ = QFileDialog.getSaveFileName(
            self, "导出辅助线", "", "JSON文件 (*.json)"
        )
        
        if not file_path:  # 用户取消了选择
            return
            
        # 确保文件有.json扩展名
        if not file_path.endswith('.json'):
            file_path += '.json'
            
        try:
            # 收集辅助线数据
            lines_data = []
            
            for line in self.image_view.auxiliary_lines:
                line_coords = line.line()
                line_data = {
                    'x1': line_coords.x1(),
                    'y1': line_coords.y1(),
                    'x2': line_coords.x2(),
                    'y2': line_coords.y2(),
                    'color': 'yellow',  # 当前硬编码为黄色
                    'width': 1  # 当前硬编码为1像素宽
                }
                lines_data.append(line_data)
                
            # 创建导出数据
            export_data = {
                'auxiliary_lines': lines_data,
                'export_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'version': '1.0'
            }
            
            # 保存到文件
            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(export_data, f, indent=2, ensure_ascii=False)
                
            QMessageBox.information(self, "导出成功", f"已成功导出{len(lines_data)}条辅助线到:\n{file_path}")
            
        except Exception as e:
            QMessageBox.critical(self, "导出失败", f"导出辅助线时出错:\n{str(e)}")

    def import_auxiliary_lines(self):
        """从JSON文件导入辅助线"""
        # 打开文件对话框
        file_path, _ = QFileDialog.getOpenFileName(self, "导入辅助线", "", "JSON文件 (*.json)")
        
        if not file_path:  # 用户取消了选择
            return
            
        try:
            # 读取文件
            with open(file_path, 'r', encoding='utf-8') as f:
                import_data = json.load(f)
                
            # 验证数据格式
            if not isinstance(import_data, dict) or 'auxiliary_lines' not in import_data:
                QMessageBox.warning(self, "导入失败", "选择的文件不是有效的辅助线配置文件")
                return
                
            lines_data = import_data['auxiliary_lines']
            if not lines_data:
                QMessageBox.information(self, "导入辅助线", "文件中没有辅助线数据")
                return
                
            # 询问是否清除现有辅助线
            clear_existing = False
            if self.image_view and self.image_view.auxiliary_lines:
                msg = "是否清除当前的辅助线后再导入？"
                reply = QMessageBox.question(self, "导入辅助线", msg, QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
                
                if reply == QMessageBox.Cancel:
                    return
                elif reply == QMessageBox.Yes:
                    clear_existing = True
            
            # 清除现有辅助线（如果需要）
            if clear_existing:
                self.image_view.clear_auxiliary_lines()
            
            # 导入辅助线
            count = 0
            for line_data in lines_data:
                try:
                    if not all(k in line_data for k in ['x1', 'y1', 'x2', 'y2']):
                        continue
                        
                    start_pos = QPointF(line_data['x1'], line_data['y1'])
                    end_pos = QPointF(line_data['x2'], line_data['y2'])
                    
                    # 添加辅助线
                    self.image_view.add_auxiliary_line(start_pos, end_pos)
                    count += 1
                except Exception as e:
                    print(f"导入辅助线时出错: {e}")
                    
            QMessageBox.information(self, "导入成功", f"已成功导入{count}条辅助线")
            
        except Exception as e:
            QMessageBox.critical(self, "导入失败", f"导入辅助线时出错:\n{str(e)}")
    
    
    def show_report_dialog(self):
        """显示报告对话框"""
        try:
            dialog = QDialog(self)
            dialog.setWindowTitle("报告展示")
            dialog.setMinimumSize(1080, 400)  # 增加记录窗口宽度，从600增加到800
            
            layout = QVBoxLayout(dialog)
            
            # 创建选项卡
            tab_widget = QTabWidget()
            layout.addWidget(tab_widget)
            
            # 点击记录选项卡
            record_tab = QWidget()
            record_layout = QVBoxLayout(record_tab)
            
            record_tree = QTreeWidget()
            record_tree.setHeaderLabels(["文件名", "时间", "RA", "DEC", "坐标", "坐标差值", "速度(像素/小时)"])
            record_layout.addWidget(record_tree)
            
            # 设置右键菜单
            record_tree.setContextMenuPolicy(Qt.CustomContextMenu)
            record_tree.customContextMenuRequested.connect(
                lambda pos: self.show_record_context_menu(pos, record_tree)
            )
            
            # 启用多选
            record_tree.setSelectionMode(QAbstractItemView.ExtendedSelection)
            
            # 按时间排序记录
            sorted_records = sorted(self.click_records, key=lambda x: x['timestamp'])
            
            # 添加记录
            prev_x, prev_y = None, None
            prev_timestamp = None
            
            for i, record in enumerate(sorted_records):
                # 提取当前坐标
                coord_str = record['coordinates'].strip('()')
                x, y = map(int, coord_str.split(','))
                
                # 获取RA和DEC坐标（如果有）
                ra_str = ""
                dec_str = ""
                if 'wcs_coordinates' in record and record['wcs_coordinates'] and 'ra_hms' in record and 'dec_dms' in record:
                    ra_str = record['ra_hms']
                    dec_str = record['dec_dms']
                
                # 计算坐标差值和速度
                if i > 0 and prev_timestamp is not None:
                    # 计算坐标差值
                    dx = x - prev_x
                    dy = y - prev_y
                    coord_diff = f"Δx={dx}, Δy={dy}"
                    
                    # 计算时间差（小时）
                    time_diff = (record['timestamp'] - prev_timestamp).total_seconds() / 3600
                    
                    # 计算速度（像素/小时）
                    if time_diff > 0:
                        speed = round(((dx**2 + dy**2)**0.5) / time_diff, 1)
                        speed_str = f"{speed}"
                    else:
                        speed_str = "N/A"
                else:
                    coord_diff = ""
                    speed_str = ""
                
                item = QTreeWidgetItem([
                    record['filename'],
                    record['time'],
                    ra_str,
                    dec_str,
                    record['coordinates'],
                    coord_diff,
                    speed_str
                ])
                record_tree.addTopLevelItem(item)
                
                # 更新前一个记录的信息
                prev_x, prev_y = x, y
                prev_timestamp = record['timestamp']
            
            # 设置列宽度
            header = record_tree.header()
            header.setSectionResizeMode(0, QHeaderView.Interactive)  # 文件名列
            header.setSectionResizeMode(1, QHeaderView.Interactive)  # 时间列 
            header.setSectionResizeMode(3, QHeaderView.Interactive)  # RA列
            header.setSectionResizeMode(4, QHeaderView.Interactive)  # DEC列
            header.setSectionResizeMode(2, QHeaderView.Interactive)  # 坐标列
            header.setSectionResizeMode(5, QHeaderView.Interactive)  # 坐标差值列
            header.setSectionResizeMode(6, QHeaderView.Interactive)  # 速度列
            
            tab_widget.addTab(record_tab, "点击记录")
            
            # 报告选项卡
            report_tab = QWidget()
            report_layout = QVBoxLayout(report_tab)
            
            report_text = QTextEdit()
            report_text.setReadOnly(True)
            report_layout.addWidget(report_text)
            
            # 不在这里生成报告内容，而是等待用户点击生成报告按钮
            report_text.setHtml("<p>点击\"生成报告\"按钮生成报告内容</p>")
            
            tab_widget.addTab(report_tab, "报告内容")
            
            # 按钮区域
            button_layout = QHBoxLayout()
            
            generate_btn = QPushButton("生成报告")
            generate_btn.clicked.connect(self.generate_report)
            button_layout.addWidget(generate_btn)
            
            # 添加清空记录按钮
            clear_btn = QPushButton("清空记录")
            clear_btn.clicked.connect(lambda: self.clear_click_records(record_tree))
            button_layout.addWidget(clear_btn)
            
            close_btn = QPushButton("关闭")
            close_btn.clicked.connect(dialog.accept)
            button_layout.addWidget(close_btn)
            
            layout.addLayout(button_layout)
            
            # 显示对话框
            dialog.exec_()
        except Exception as e:
            print(f"显示报告对话框时出错: {e}")
            QMessageBox.critical(self, "错误", f"显示报告对话框时出错: {e}")
    
    def show_report_coord_dialog(self):
        """显示报告坐标对话框，用于解析报告时间和坐标并标记到对应图片"""
        try:
            dialog = QDialog(self)
            dialog.setWindowTitle("报告坐标功能")
            dialog.setMinimumSize(600, 400)
            
            layout = QVBoxLayout(dialog)
            
            # 添加说明标签
            info_label = QLabel("请输入报告坐标数据，格式为：HH:MM x y，例如 20:30 426 1057")
            layout.addWidget(info_label)
            
            # 添加文本输入框
            text_edit = QTextEdit()
            text_edit.setPlaceholderText("20:30 426 1057\n20:45 437 1054\n21:00 449 1052\n21:15 459 1050")
            layout.addWidget(text_edit)
            
            # 按钮区域
            button_layout = QHBoxLayout()
            
            mark_btn = QPushButton("标记坐标")
            mark_btn.clicked.connect(lambda: self.mark_report_coordinates(text_edit.toPlainText()))
            button_layout.addWidget(mark_btn)
            
            clear_btn = QPushButton("清除标记")
            clear_btn.clicked.connect(self.clear_report_markers)
            button_layout.addWidget(clear_btn)
            
            close_btn = QPushButton("关闭")
            close_btn.clicked.connect(dialog.accept)
            button_layout.addWidget(close_btn)
            
            layout.addLayout(button_layout)
            
            # 显示对话框
            dialog.exec_()
        except Exception as e:
            print(f"显示报告坐标对话框时出错: {e}")
            QMessageBox.critical(self, "错误", f"显示报告坐标对话框时出错: {e}")
    
    def convert_time_format(self, time_str):
        # 统一处理带/不带冒号的时间格式
        if ':' in time_str:
            return time_str
        # 根据长度判断格式
        if len(time_str) == 6:
            return f"{time_str[:2]}:{time_str[2:4]}:{time_str[4:6]}"
        elif len(time_str) == 4:
            return f"{time_str[:2]}:{time_str[2:4]}"
        else:
            # 保留原格式，避免错误
            return time_str

    def mark_report_coordinates(self, report_text):
        """解析报告文本并标记坐标"""
        try:
            if not self.images:
                QMessageBox.warning(self, "警告", "请先加载图片！")
                return
            
            # 清除之前的报告标记
            self.clear_report_markers()
            
            # 初始化报告标记列表（如果不存在）
            if not hasattr(self, 'report_markers'):
                self.report_markers = []
            
            # 解析报告文本
            lines = [line.strip() for line in report_text.replace('\r', '').splitlines() if line.strip()]
            coord_data = []
            
            for line in lines:
                parts = line.strip().split()
                if len(parts) >= 3:
                    try:
                        # 预处理时间格式
                        raw_time = parts[0]
                        if ':' not in raw_time:
                            if len(raw_time) == 4:
                                raw_time = f"{raw_time[:2]}:{raw_time[2:]}"
                            elif len(raw_time) == 6:
                                raw_time = f"{raw_time[:2]}:{raw_time[2:4]}:{raw_time[4:]}"
                        
                        time_str = self.convert_time_format(raw_time)
                        x = int(parts[1])
                        y = int(parts[2])
                        coord_data.append({'time': time_str, 'x': x, 'y': y})
                    except (ValueError, IndexError) as e:
                        print(f"解析行 '{line}' 时出错: {e}")
            
            if not coord_data:
                QMessageBox.warning(self, "警告", "未找到有效的坐标数据！")
                return
            
            # 匹配图片并标记坐标
            marked_count = 0
            for image in self.images:
                filename = os.path.basename(image['filename'])
                # 从文件名中提取时间信息，格式如 CCOR1_1B_20250318T003025_V00_NC_processed.fits
                time_match = re.search(r'T[^\d]*(\d{2})[^\d]*(\d{2})', filename)
                if time_match:
                    hour = time_match.group(1)
                    minute = time_match.group(2)
                    image_time = f"{hour}:{minute}"
                    
                    # 查找匹配的坐标数据
                    for data in coord_data:
                        if self.convert_time_format(data['time']) == self.convert_time_format(image_time):
                            # 标记坐标
                            self.mark_coordinate_on_image(image, data['x'], data['y'])
                            marked_count += 1
            
            if marked_count > 0:
                QMessageBox.information(self, "成功", f"已在{marked_count}张图片上标记坐标！")
            else:
                QMessageBox.warning(self, "警告", "未找到匹配的图片时间！")
                
        except Exception as e:
            print(f"标记报告坐标时出错: {e}")
            QMessageBox.critical(self, "错误", f"标记报告坐标时出错: {e}")
    
    def mark_coordinate_on_image(self, image, x, y):
        """在指定图片上标记坐标"""
        try:
            # 创建标记信息
            marker_info = {
                'image_filename': image['filename'],
                'x': x,
                'y': y
            }
            
            # 添加到报告标记列表
            self.report_markers.append(marker_info)
            
            # 如果是当前显示的图片，立即显示标记
            if self.current_index >= 0 and self.images[self.current_index]['filename'] == image['filename']:
                self.add_report_marker_to_current_image(x, y)
                
        except Exception as e:
            print(f"在图片上标记坐标时出错: {e}")
    
    def add_report_marker_to_current_image(self, x, y):
        """在当前图片上添加报告标记"""
        try:
            if not self.image_view or not self.image_view.scene:
                return
                
            # 创建黄色圆圈标记
            pos = QPointF(x, y)
            size = 15  # 圆形大小
            circle = self.image_view.scene.addEllipse(
                pos.x() - size/2, pos.y() - size/2, 
                size, size, 
                QPen(QColor('yellow'), 1.5),  # 黄色线条
                QBrush(Qt.transparent)  # 中心透明
            )
            circle.setZValue(20)  # 确保在最上层
            
            # 存储临时标记
            if not hasattr(self.image_view, 'report_circle_markers'):
                self.image_view.report_circle_markers = []
            self.image_view.report_circle_markers.append(circle)
            
        except Exception as e:
            print(f"添加报告标记时出错: {e}")
    
    def clear_report_markers(self):
        """清除所有报告标记"""
        try:
            # 清除标记列表
            if hasattr(self, 'report_markers'):
                self.report_markers = []
            
            # 清除当前图像上的标记
            if hasattr(self, 'image_view') and self.image_view:
                if hasattr(self.image_view, 'report_circle_markers'):
                    for marker in self.image_view.report_circle_markers:
                        if marker and marker.scene() == self.image_view.scene:
                            try:
                                self.image_view.scene.removeItem(marker)
                            except RuntimeError:
                                pass  # 忽略已删除对象的错误
                    self.image_view.report_circle_markers = []
            
            # 如果在全屏模式下，也清除全屏窗口的标记
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                fullscreen_view = self.fullscreen_window.centralWidget()
                if isinstance(fullscreen_view, ImageGraphicsView):
                    if hasattr(fullscreen_view, 'report_circle_markers'):
                        for marker in fullscreen_view.report_circle_markers:
                            if marker and marker.scene() == fullscreen_view.scene:
                                try:
                                    fullscreen_view.scene.removeItem(marker)
                                except RuntimeError:
                                    pass
                        fullscreen_view.report_circle_markers = []
            
        except Exception as e:
            print(f"清除报告标记时出错: {e}")
    
    def clear_click_records(self, tree_widget=None):
        """清空所有点击记录"""
        try:
            # 确认是否清空
            reply = QMessageBox.question(self, "确认清空", 
                                        "确定要清空所有点击记录吗？此操作不可恢复。",
                                        QMessageBox.Yes | QMessageBox.No, 
                                        QMessageBox.No)
            
            if reply == QMessageBox.Yes:
                # 清空记录列表
                self.click_records.clear()
                
                # 如果提供了树控件，清空它
                if tree_widget:
                    tree_widget.clear()
                
                # 清除当前图像上的所有标记
                if self.image_view:
                    self.image_view.clear_markers()
                
                # 如果在全屏模式下，也清除全屏窗口的标记
                if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                    fullscreen_view = self.fullscreen_window.centralWidget()
                    if isinstance(fullscreen_view, ImageGraphicsView):
                        fullscreen_view.clear_markers()
                
                # 显示成功消息
                self.statusBar().showMessage("已清空所有点击记录")
                QMessageBox.information(self, "清空成功", "已清空所有点击记录")
        except Exception as e:
            print(f"清空点击记录时出错: {e}")
            QMessageBox.critical(self, "错误", f"清空点击记录时出错: {e}")
    
    def generate_report_content(self):
        """生成报告内容"""
        if not self.click_records:
            return "<p>没有点击记录</p>"
            
        content = "<h2>点击记录报告</h2>"
        content += "<table border='1' cellspacing='0' cellpadding='5'>"
        content += "<tr><th>文件名</th><th>时间</th><th>坐标</th></tr>"
        
        for record in self.click_records:
            content += f"<tr><td>{record['filename']}</td><td>{record['time']}</td><td>{record['coordinates']}</td></tr>"
            
        content += "</table>"
        
        return content

    def show_speed_dialog(self):
        """显示播放速度调节对话框"""
        try:
            dialog = QDialog(self)
            dialog.setWindowTitle("播放速度调节")
            dialog.setFixedSize(300, 150)
            
            layout = QVBoxLayout(dialog)
            
            # 添加说明标签
            info_label = QLabel("设置播放速度 (0.5x - 5.0x)")
            info_label.setAlignment(Qt.AlignCenter)
            layout.addWidget(info_label)
            
            # 添加滑块
            slider = QSlider(Qt.Horizontal)
            slider.setRange(5, 50)  # 0.5x-5x倍速
            slider.setValue(self.current_play_speed)  # 使用当前保存的播放速度值
            layout.addWidget(slider)
            
            # 添加显示标签
            value_label = QLabel(f"{slider.value() / 10.0:.1f}倍速")
            value_label.setAlignment(Qt.AlignCenter)
            layout.addWidget(value_label)
            
            # 连接滑块值变化信号
            slider.valueChanged.connect(lambda v: value_label.setText(f"{v / 10.0:.1f}倍速"))
            
            # 按钮区域
            button_layout = QHBoxLayout()
            
            ok_btn = QPushButton("确定")
            ok_btn.clicked.connect(lambda: self.update_speed(slider.value()))
            ok_btn.clicked.connect(dialog.accept)
            button_layout.addWidget(ok_btn)
            
            cancel_btn = QPushButton("取消")
            cancel_btn.clicked.connect(dialog.reject)
            button_layout.addWidget(cancel_btn)
            
            layout.addLayout(button_layout)
            
            # 显示对话框
            if dialog.exec_() == QDialog.Accepted:
                speed_value = slider.value() / 10.0
                self.statusBar().showMessage(f"播放速度已设置为 {speed_value:.1f} 倍速")
        except Exception as e:
            print(f"显示速度对话框时出错: {e}")
            self.statusBar().showMessage(f"显示速度对话框失败: {e}")

    def show_auto_animation_dialog(self):
        dialog = QDialog(self)
        dialog.setWindowTitle("自动动画")
        dialog.setFixedSize(500, 240)
        layout = QVBoxLayout(dialog)
        form = QFormLayout()
        hist_layout = QHBoxLayout()
        hist_edit = QLineEdit()
        hist_edit.setReadOnly(True)
        if self.config.has_section("AutoAnimation") and self.config.has_option("AutoAnimation", "history_folder"):
            hist_edit.setText(self.config["AutoAnimation"]["history_folder"])
        hist_btn = QPushButton("浏览...")
        hist_layout.addWidget(hist_edit)
        hist_layout.addWidget(hist_btn)
        form.addRow("历史图库文件夹:", hist_layout)
        new_layout = QHBoxLayout()
        new_edit = QLineEdit()
        new_edit.setReadOnly(True)
        if self.config.has_section("AutoAnimation") and self.config.has_option("AutoAnimation", "new_folder"):
            new_edit.setText(self.config["AutoAnimation"]["new_folder"])
        new_btn = QPushButton("浏览...")
        new_layout.addWidget(new_edit)
        new_layout.addWidget(new_btn)
        form.addRow("新图文件夹:", new_layout)
        speed_spin = QDoubleSpinBox()
        speed_spin.setRange(0.1, 5.0)
        speed_spin.setSingleStep(0.1)
        default_interval = 0.5
        if self.config.has_section("AutoAnimation") and self.config.has_option("AutoAnimation", "interval_sec"):
            try:
                default_interval = float(self.config["AutoAnimation"]["interval_sec"]) or 0.5
            except Exception:
                default_interval = 0.5
        speed_spin.setValue(default_interval)
        form.addRow("播放间隔(秒):", speed_spin)
        layout.addLayout(form)
        btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        ok_btn = btns.button(QDialogButtonBox.Ok)
        ok_btn.setEnabled(os.path.isdir(hist_edit.text()) and os.path.isdir(new_edit.text()))
        layout.addWidget(btns)
        def browse_hist():
            folder = QFileDialog.getExistingDirectory(dialog, "选择历史图库文件夹", hist_edit.text() or "")
            if folder:
                hist_edit.setText(folder)
                if not self.config.has_section("AutoAnimation"):
                    self.config.add_section("AutoAnimation")
                self.config["AutoAnimation"]["history_folder"] = folder
                self.save_config()
                ok_btn.setEnabled(os.path.isdir(hist_edit.text()) and os.path.isdir(new_edit.text()))
        def browse_new():
            folder = QFileDialog.getExistingDirectory(dialog, "选择新图文件夹", new_edit.text() or "")
            if folder:
                new_edit.setText(folder)
                if not self.config.has_section("AutoAnimation"):
                    self.config.add_section("AutoAnimation")
                self.config["AutoAnimation"]["new_folder"] = folder
                self.save_config()
                ok_btn.setEnabled(os.path.isdir(hist_edit.text()) and os.path.isdir(new_edit.text()))
        hist_btn.clicked.connect(browse_hist)
        new_btn.clicked.connect(browse_new)
        btns.accepted.connect(lambda: self.prepare_auto_animation(hist_edit.text(), new_edit.text(), speed_spin.value()))
        btns.accepted.connect(dialog.accept)
        btns.rejected.connect(dialog.reject)
        dialog.exec_()

    def prepare_auto_animation(self, history_folder, new_folder, interval_sec):
        try:
            self.animation_sets = []
            new_files = sorted([f for f in os.listdir(new_folder) if f.lower().endswith((".fit", ".fits", ".fts"))])
            old_files_set = set(os.listdir(history_folder))
            for nf in new_files:
                if nf in old_files_set:
                    new_path = os.path.join(new_folder, nf)
                    old_path = os.path.join(history_folder, nf)
                else:
                    base = os.path.splitext(nf)[0]
                    matches = [f for f in old_files_set if os.path.splitext(f)[0] == base]
                    if not matches:
                        continue
                    old_path = os.path.join(history_folder, matches[0])
                    new_path = os.path.join(new_folder, nf)
                try:
                    new_img = fits.getdata(new_path)
                    old_img = fits.getdata(old_path)
                    if new_img is None or old_img is None:
                        continue
                    if len(new_img.shape) > 2:
                        new_img = new_img[0]
                    if len(old_img.shape) > 2:
                        old_img = old_img[0]
                    new_img = new_img.astype(np.float32)
                    old_img = old_img.astype(np.float32)
                    aligned_new, _ = aa.register(new_img, old_img)
                    def to_pixmap(arr):
                        flat = arr.flatten()
                        mn = np.percentile(flat, 1.0)
                        mx = np.percentile(flat, 99.5)
                        if mx > mn:
                            st = np.clip(arr, mn, mx)
                            norm = ((st - mn) / (mx - mn) * 255).astype(np.uint8)
                        else:
                            norm = np.zeros_like(arr, dtype=np.uint8)
                        h, w = norm.shape[:2]
                        qimg = QImage(norm.data, w, h, w, QImage.Format_Grayscale8)
                        return QPixmap.fromImage(qimg)
                    old_pm = to_pixmap(old_img)
                    new_pm = to_pixmap(aligned_new)
                    try:
                        tmp_proc = FitsImageProcessor()
                        tmp_proc.set_auto_parse_wcs(True)
                        tmp_proc.load_fits(old_path)
                        old_wcs = tmp_proc.get_wcs()
                        old_pix_scale = tmp_proc.get_pixel_scale()
                        old_header = tmp_proc.header
                    except Exception:
                        old_wcs = None
                        old_pix_scale = None
                        old_header = None
                    self.animation_sets.append({
                        "name": os.path.splitext(os.path.basename(nf))[0],
                        "old_pm": old_pm,
                        "new_pm": new_pm,
                        "old_path": old_path,
                        "new_path": new_path,
                        "old_arr": old_img,
                        "new_arr": aligned_new,
                        "old_wcs": old_wcs,
                        "old_pixel_scale": old_pix_scale,
                        "old_header": old_header
                    })
                except Exception:
                    continue
            if not self.animation_sets:
                QMessageBox.information(self, "提示", "未生成动画")
                return
            self.animation_mode = True
            self.current_animation_index = 0
            self._anim_toggle = False
            self._auto_anim_interval_ms = int(max(0.1, float(interval_sec)) * 1000)
            if not self.config.has_section("AutoAnimation"):
                self.config.add_section("AutoAnimation")
            self.config["AutoAnimation"]["interval_sec"] = str(interval_sec)
            self.save_config()
            self.show_animation_frame()
            try:
                self.play_timer.timeout.disconnect()
            except Exception:
                pass
            self.play_timer.timeout.connect(self.toggle_animation_frame)
            self.is_playing = True
            self.play_timer.start(self._auto_anim_interval_ms)
            self.pause_action.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
            self.pause_action.setText("暂停")
            self.prev_image_action.setEnabled(False)
            self.next_image_action.setEnabled(False)
            try:
                self.image_view.setFocusPolicy(Qt.StrongFocus)
                self.image_view.setFocus()
            except Exception:
                pass
            self.statusBar().showMessage(f"自动动画 {len(self.animation_sets)} 组，间隔 {interval_sec:.2f} 秒")
        except Exception as e:
            QMessageBox.critical(self, "错误", str(e))

    def show_animation_frame(self):
        if not self.animation_mode or not self.animation_sets:
            return
        idx = self.current_animation_index % len(self.animation_sets)
        item = self.animation_sets[idx]
        pm = item["old_pm"] if not self._anim_toggle else item["new_pm"]
        try:
            if not hasattr(self, 'image_processor') or self.image_processor is None:
                self.image_processor = FitsImageProcessor()
            # 使用参考(历史)图的WCS，aligned_new与old已在同一像素网格
            if item.get("old_wcs") is not None:
                try:
                    self.image_processor.wcs = item.get("old_wcs")
                except Exception:
                    pass
            if item.get("old_pixel_scale") is not None:
                try:
                    self.image_processor.pixel_scale = item.get("old_pixel_scale")
                except Exception:
                    pass
            if self._anim_toggle:
                self.image_processor.image_data = item.get("new_arr")
            else:
                self.image_processor.image_data = item.get("old_arr")
            if item.get("old_header") is not None:
                try:
                    self.image_processor.header = item.get("old_header")
                except Exception:
                    pass
        except Exception:
            pass
        self.image_view.set_image(pm, filename=item["name"])
        self.zoom_label.setText(f'缩放: {self.image_view.zoom_factor:.1f}x')
        self.filename_label.setText(item["name"])

    def toggle_animation_frame(self):
        if not self.animation_mode:
            return
        self._anim_toggle = not self._anim_toggle
        self.show_animation_frame()

    def change_animation(self, direction):
        if not self.animation_mode or not self.animation_sets:
            return
        self.current_animation_index = (self.current_animation_index + direction) % len(self.animation_sets)
        self._anim_toggle = False
        self.show_animation_frame()

    def display_animation_difference(self):
        if not self.animation_sets:
            return
        idx = self.current_animation_index % len(self.animation_sets)
        item = self.animation_sets[idx]
        a = item.get("new_arr")
        b = item.get("old_arr")
        if a is None or b is None:
            return
        a = a.astype(np.float32)
        b = b.astype(np.float32)
        fa = a.flatten()
        fb = b.flatten()
        mn = min(np.percentile(fa, 1.0), np.percentile(fb, 1.0))
        mx = max(np.percentile(fa, 99.5), np.percentile(fb, 99.5))
        if mx <= mn:
            return
        an = np.clip(a, mn, mx)
        bn = np.clip(b, mn, mx)
        an = ((an - mn) / (mx - mn) * 255).astype(np.uint8)
        bn = ((bn - mn) / (mx - mn) * 255).astype(np.uint8)
        diff = np.clip(an.astype(np.int16) - bn.astype(np.int16), 0, 255).astype(np.uint8)
        h, w = diff.shape[:2]
        qimg = QImage(diff.data, w, h, w, QImage.Format_Grayscale8)
        pm = QPixmap.fromImage(qimg)
        self.image_view.set_image(pm, filename=item["name"] + "_差异叠加")
        self.zoom_label.setText(f'缩放: {self.image_view.zoom_factor:.1f}x')
        self.filename_label.setText(item["name"] + "_差异叠加")

    def show_goto_dialog(self):
        """显示坐标定位对话框"""
        if not self.images or self.current_index < 0:
            QMessageBox.information(self, "提示", "请先加载图片")
            return
            
        dialog = QDialog(self)
        dialog.setWindowTitle("坐标定位")
        dialog.setFixedSize(300, 150)
        
        layout = QVBoxLayout(dialog)
        
        # 坐标输入区域
        coord_layout = QHBoxLayout()
        
        x_layout = QHBoxLayout()
        x_layout.addWidget(QLabel("X:"))
        x_input = QLineEdit()
        x_input.setPlaceholderText("X坐标")
        x_layout.addWidget(x_input)
        
        y_layout = QHBoxLayout()
        y_layout.addWidget(QLabel("Y:"))
        y_input = QLineEdit()
        y_input.setPlaceholderText("Y坐标")
        y_layout.addWidget(y_input)
        
        coord_layout.addLayout(x_layout)
        coord_layout.addLayout(y_layout)
        
        layout.addLayout(coord_layout)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        goto_btn = QPushButton("跳转定位")
        goto_btn.clicked.connect(lambda: self.goto_coordinate_from_dialog(x_input.text(), y_input.text()))
        goto_btn.clicked.connect(dialog.accept)
        button_layout.addWidget(goto_btn)
        
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(dialog.reject)
        button_layout.addWidget(cancel_btn)
        
        layout.addLayout(button_layout)
        
        # 显示对话框
        dialog.exec_()

    def show_goto_radec_dialog(self):
        """显示天文坐标定位对话框"""
        if not self.images or self.current_index < 0:
            QMessageBox.information(self, "提示", "请先加载图片")
            return
            
        # 检查当前图像是否有WCS信息
        from coordinate_parser import parse_ra_dec_coordinates
        import re
        
        dialog = QDialog(self)
        dialog.setWindowTitle("天文坐标定位")
        dialog.setFixedSize(350, 200)
        
        layout = QVBoxLayout(dialog)
        
        # 添加说明标签
        info_label = QLabel("支持格式: 十进制度数、HH:MM:SS、HH MM SS")
        info_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(info_label)
        
        # 坐标输入区域
        coord_layout = QVBoxLayout()
        
        ra_layout = QHBoxLayout()
        ra_layout.addWidget(QLabel("赤经(RA):"))
        ra_input = QLineEdit()
        ra_input.setPlaceholderText("例如: 12:34:56.7 或 12 34 56.7 或 123.45")
        ra_layout.addWidget(ra_input)
        
        dec_layout = QHBoxLayout()
        dec_layout.addWidget(QLabel("赤纬(DEC):"))
        dec_input = QLineEdit()
        dec_input.setPlaceholderText("例如: +12:34:56.7 或 -12 34 56.7 或 -12.345")
        dec_layout.addWidget(dec_input)
        
        coord_layout.addLayout(ra_layout)
        coord_layout.addLayout(dec_layout)
        
        layout.addLayout(coord_layout)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        goto_btn = QPushButton("跳转定位")
        goto_btn.clicked.connect(lambda: self.goto_radec_coordinate_from_dialog(ra_input.text(), dec_input.text()))
        goto_btn.clicked.connect(dialog.accept)
        button_layout.addWidget(goto_btn)
        
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(dialog.reject)
        button_layout.addWidget(cancel_btn)
        
        layout.addLayout(button_layout)
        
        # 显示对话框
        dialog.exec_()

    def goto_coordinate_from_dialog(self, x_text, y_text):
        """从对话框跳转到指定坐标"""
        if not self.image_view or self.current_index < 0:
            return
            
        try:
            # 获取用户输入的坐标
            x = int(x_text)
            y = int(y_text)
            
            # 跳转到指定位置
            success = self.image_view.goto_position(x, y)
            
            # 只在失败时显示弹窗提示，成功则不显示任何提示
            if not success:
                QMessageBox.warning(self, "坐标错误", "坐标超出图像范围")
        except ValueError:
            QMessageBox.warning(self, "输入错误", "请输入有效的整数坐标")

    def goto_radec_coordinate_from_dialog(self, ra_text, dec_text):
        """从对话框跳转到指定天文坐标（赤经/赤纬）"""
        if not self.image_view or self.current_index < 0:
            return
            
        # 检查当前图像是否有WCS信息
        if not hasattr(self, 'image_processor') or not self.image_processor or not hasattr(self.image_processor, 'wcs') or not self.image_processor.wcs:
            QMessageBox.warning(self, "WCS错误", "当前图像没有WCS信息，无法进行天文坐标定位")
            return
            
        try:
            # 解析赤经赤纬坐标
            from coordinate_parser import parse_ra_dec_coordinates
            ra_deg, dec_deg = parse_ra_dec_coordinates(ra_text, dec_text)
            
            # 使用WCS将天文坐标转换为像素坐标
            from coordinate_parser import CoordinateParser
            coord_parser = CoordinateParser(self.settings_manager)
            coord_parser.set_wcs(self.image_processor.wcs)
            pixel_coords = coord_parser.world_to_pixel(ra_deg, dec_deg)
            
            if pixel_coords is None:
                QMessageBox.warning(self, "坐标转换错误", "无法将天文坐标转换为像素坐标")
                return
                
            x, y = pixel_coords
            
            # 跳转到指定位置
            success = self.image_view.goto_position(int(x), int(y))
            
            # 只在失败时显示弹窗提示，成功则显示坐标信息
            if not success:
                QMessageBox.warning(self, "坐标错误", "坐标超出图像范围")
            else:
                # 添加到历史记录
                history_record = self.history_logger.add_record(
                    "天文坐标定位", 
                    f"定位到天文坐标 RA={ra_deg:.6f}°, DEC={dec_deg:.6f}°，像素坐标 X={int(x)}, Y={int(y)}"
                )
                self.update_history_display(history_record)
                
                # 在状态栏显示信息
                self.statusBar().showMessage(f"已定位到天文坐标: RA={ra_deg:.6f}°, DEC={dec_deg:.6f}°")
        except ValueError as e:
            QMessageBox.warning(self, "输入错误", f"坐标格式错误: {str(e)}")
        except Exception as e:
            QMessageBox.warning(self, "处理错误", f"处理天文坐标时出错: {str(e)}")

    def adjust_stack_settings(self):
        """调整叠加设置"""
        if not self.is_stacked:
            return
            
        # 显示设置对话框
        if self.show_stack_settings():
            # 用户确认了设置，更新叠加图像
            self.stacked_image = self.create_stacked_image()
            
            # 显示更新后的叠加图像
            if self.stacked_image is not None:
                # 保存当前缩放因子
                current_zoom = self.zoom_slider.value()
                # 显示叠加图像，并在文件名中包含设置信息
                stack_info = f"叠加图像 [亮度:{self.brightness_value}% 对比度:{self.contrast_value}%]"
                self.image_view.set_image(self.stacked_image, filename=stack_info)
                # 恢复原来的缩放因子
                self.update_zoom(current_zoom)
                
                # 添加到历史记录
                history_record = self.history_logger.add_record(
                    "图像叠加", 
                    f"更新叠加设置，亮度:{self.brightness_value}%，对比度:{self.contrast_value}%"
                )
                self.update_history_display(history_record)
            else:
                QMessageBox.warning(self, "警告", "更新叠加图像失败！")
    
    def show_record_context_menu(self, position, tree_widget):
        """显示记录的右键菜单"""
        try:
            # 获取选中的项
            selected_items = tree_widget.selectedItems()
            if not selected_items:
                return
                
            # 创建菜单
            menu = QMenu()
            copy_action = menu.addAction("复制选中行")
            copy_ra_dec_action = menu.addAction("复制RA/DEC坐标")
            menu.addSeparator()
            delete_action = menu.addAction("删除选中记录")
            
            # 显示菜单并获取选择的操作
            action = menu.exec_(tree_widget.mapToGlobal(position))
            
            # 处理操作
            if action == delete_action:
                self.delete_selected_records(tree_widget)
            elif action == copy_action:
                self.copy_selected_cells(tree_widget)
            elif action == copy_ra_dec_action:
                self.copy_ra_dec_coordinates(tree_widget)
                
        except Exception as e:
            print(f"显示右键菜单时出错: {e}")
            QMessageBox.critical(self, "错误", f"显示右键菜单时出错: {e}")
    
    def delete_selected_records(self, tree_widget):
        """删除选中的记录"""
        try:
            # 获取选中的项
            selected_items = tree_widget.selectedItems()
            if not selected_items:
                return
                
            # 确认删除
            reply = QMessageBox.question(
                self, 
                "确认删除", 
                f"确定要删除选中的 {len(selected_items)} 条记录吗？",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            
            if reply == QMessageBox.Yes:
                # 记录删除前的记录数
                original_count = len(self.click_records)
                
                # 从界面和数据中删除记录
                items_to_remove = []
                records_to_remove = []
                
                for item in selected_items:
                    # 获取记录信息
                    filename = item.text(0)
                    time_info = item.text(1)
                    coordinates = item.text(4)  # 注意：坐标在第5列（索引4）
                    
                    # 在记录列表中查找匹配的记录
                    for i, record in enumerate(self.click_records):
                        if (record['filename'] == filename and 
                            record['time'] == time_info and 
                            record['coordinates'] == coordinates):
                            # 添加到待删除列表
                            records_to_remove.append(i)
                            break
                    
                    # 记录需要从树控件中移除的项
                    items_to_remove.append(item)
                
                # 从记录列表中删除（从后往前删除，避免索引变化）
                for i in sorted(records_to_remove, reverse=True):
                    if 0 <= i < len(self.click_records):
                        self.click_records.pop(i)
                
                # 从树控件中移除项
                for item in items_to_remove:
                    index = tree_widget.indexOfTopLevelItem(item)
                    if index >= 0:
                        tree_widget.takeTopLevelItem(index)
                        
                # 计算删除的记录数
                removed_count = original_count - len(self.click_records)
                QMessageBox.information(self, "提示", f"已删除 {removed_count} 条记录。")
                
                
        except Exception as e:
            print(f"删除记录时出错: {e}")
            QMessageBox.critical(self, "错误", f"删除记录时出错: {e}")
            
    def copy_selected_cells(self, tree_widget):
        """复制选中行的所有信息"""
        try:
            # 获取选中的项
            selected_items = tree_widget.selectedItems()
            if not selected_items:
                return
                
            # 准备复制的文本
            copy_text = ""
            
            # 获取表头文本作为列名
            header_texts = []
            for col in range(tree_widget.columnCount()):
                header_texts.append(tree_widget.headerItem().text(col))
            
            # 要排除的列名
            excluded_columns = ["坐标差值", "速度(像素/小时)"]
            
            # 复制选中行的所有信息
            for item in selected_items:
                row_data = []
                # 添加列名和对应的值，排除指定列
                for col in range(tree_widget.columnCount()):
                    col_name = header_texts[col]
                    # 跳过要排除的列
                    if col_name in excluded_columns:
                        continue
                    cell_text = item.text(col)
                    row_data.append(f"{col_name}: {cell_text}")
                
                # 将该行数据添加到复制文本中
                copy_text += "\n".join(row_data) + "\n\n"
            
            # 复制到剪贴板
            if copy_text:
                clipboard = QApplication.clipboard()
                clipboard.setText(copy_text.strip())
                self.statusBar().showMessage("已复制选中行的所有信息到剪贴板", 3000)
            
        except Exception as e:
            print(f"复制选中行时出错: {e}")
            QMessageBox.critical(self, "错误", f"复制选中行时出错: {e}")
    
    def copy_ra_dec_coordinates(self, tree_widget):
        """复制RA/DEC坐标"""
        try:
            # 获取选中的项
            selected_items = tree_widget.selectedItems()
            if not selected_items:
                return
                
            # 准备复制的文本
            copy_text = ""
            
            # RA和DEC列的索引
            ra_column = 2  # RA在第3列（索引2）
            dec_column = 3  # DEC在第4列（索引3）
            
            for item in selected_items:
                ra = item.text(ra_column)
                dec = item.text(dec_column)
                if ra and dec:
                    copy_text += f"{ra} {dec}\n"
            
            # 复制到剪贴板
            if copy_text:
                clipboard = QApplication.clipboard()
                clipboard.setText(copy_text.strip())
                self.statusBar().showMessage("已复制RA/DEC坐标到剪贴板", 3000)
            else:
                QMessageBox.warning(self, "警告", "选中的记录中没有有效的RA/DEC坐标")
            
        except Exception as e:
            print(f"复制RA/DEC坐标时出错: {e}")
            QMessageBox.critical(self, "错误", f"复制RA/DEC坐标时出错: {e}")

    def update_gamma_value(self, value, label):
        """更新伽马值"""
        self.gamma_value = value / 10.0  # 将滑块值转换为0.5-2.0范围
        label.setText(f"{self.gamma_value:.1f}")
        
        # 保存设置
        self.save_config()
    
    def toggle_high_performance(self, checked):
        """切换高性能模式"""
        self.high_performance = checked
        
        # 保存设置
        self.save_config()
    
    def equalize_adaptively(self, image, tile_size=8, clip_limit=2.0):
        """对图像进行自适应直方图均衡化增强"""
        # 优化：使用简单的全局直方图均衡化替代分块处理，以提高性能
        flat_image = image.flatten()
        hist, _ = np.histogram(flat_image, 256, [0, 1])
        
        # 执行对比度限制（简化版）
        if clip_limit > 0:
            hist = np.minimum(hist, clip_limit)
        
        # 计算累积分布函数
        cdf = hist.cumsum() / hist.sum()
        
        # 应用变换
        indices = np.floor(flat_image * 255).astype(np.int32)
        indices = np.clip(indices, 0, 255)
        result_flat = cdf[indices]
        
        # 重新塑形
        result = result_flat.reshape(image.shape)
        
        return result
    
    def enhance_edges(self, image):
        """边缘增强"""
        # 创建高斯模糊版本
        blurred = self.gaussian_blur(image, 3)
        
        # 应用简单的边缘检测（图像减去模糊版本）
        edges = image - blurred
        
        # 增强边缘
        return np.clip(image + edges * 2, 0, 255)
    
    def gaussian_blur(self, image, kernel_size=3):
        """高斯模糊"""
        # 简化实现：使用均值滤波替代完整的高斯滤波
        # 创建均值滤波器
        kernel = np.ones((kernel_size, kernel_size), np.float32) / (kernel_size * kernel_size)
        
        # 使用NumPy的卷积操作
        from scipy import ndimage
        # 使用ndimage的卷积函数代替手动循环
        result = ndimage.convolve(image, kernel, mode='reflect')
        
        return result
    
    def apply_track_enhancement(self, image, angle_degrees):
        """应用径迹增强"""
        # 转换角度为弧度
        angle_rad = np.radians(angle_degrees)
        
        # 计算方向向量
        dx = np.cos(angle_rad)
        dy = np.sin(angle_rad)
        
        # 确定核大小（奇数）
        kernel_size = 7
        
        # 创建方向性模糊核（简化的线性核）
        kernel = np.zeros((kernel_size, kernel_size), np.float32)
        center = kernel_size // 2
        
        # 在核中心绘制一条线，方向与指定角度一致
        for i in range(-center, center + 1):
            # 沿着方向向量移动
            x = center + int(round(i * dx))
            y = center + int(round(i * dy))
            
            # 确保坐标在核范围内
            if 0 <= x < kernel_size and 0 <= y < kernel_size:
                kernel[y, x] = 1.0
        
        # 归一化核
        if np.sum(kernel) > 0:
            kernel /= np.sum(kernel)
        
        # 使用SciPy的卷积函数
        from scipy import ndimage
        result = ndimage.convolve(image, kernel, mode='reflect')
        
        # 创建增强的图像（原始 + 方向性模糊）
        enhanced = image * 0.7 + result * 0.3
        
        return enhanced
    
    def process_stack_optimized(self):
        """优化的叠加处理方法，用于加速叠加处理流程"""
        # 重置之前的设置回调
        self.stacked_image = None
        
        # 如果没有足够的图像，直接返回失败
        if not self.images or len(self.images) < 2:
            QMessageBox.warning(self, "警告", "需要至少两张图像才能叠加！")
            return False
        
        try:
            # 使用QFuture和QtConcurrent进行并行处理
            # 显示进度对话框（使用更详细的标签）
            mode_text = "极限增强" if hasattr(self, 'enhance_mode') and self.enhance_mode == "extreme" else (
                "高级增强" if hasattr(self, 'enhance_mode') and self.enhance_mode == "advanced" else "标准")
            perf_text = "高性能" if self.high_performance else "高质量"
            progress = QProgressDialog(f"正在叠加图像... ({mode_text}模式, {perf_text})", "取消", 0, len(self.images), self)
            progress.setWindowTitle("图像叠加处理中")
            progress.setWindowModality(Qt.WindowModal)
            progress.show()
            
            # 限制处理的图像数量，防止内存溢出
            max_images_to_process = min(len(self.original_images), 50)  # 最多处理50张图片
            
            # 调用原始的create_stacked_image方法，但只使用有限数量的图像
            original_images_backup = self.original_images.copy()
            self.original_images = self.original_images[:max_images_to_process]
            
            # 调用叠加方法
            result = self.create_stacked_image()
            
            # 恢复原始图像列表
            self.original_images = original_images_backup
            
            # 关闭进度条
            progress.close()
            return result
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"优化叠加过程中出错: {str(e)}")
            import traceback
            traceback.print_exc()
            return False
    
    def save_current_image(self):
        """保存当前显示的图片或叠加图像"""
        try:
            # 如果没有图像，给出提示
            if not self.images and not self.stacked_image:
                QMessageBox.warning(self, "警告", "没有可用的图像！")
                return
            
            # 准备要保存的图像
            if self.is_stacked and self.stacked_image:
                # 叠加状态，保存叠加图像
                save_pixmap = self.stacked_image
                default_name = "stacked_image"
            else:
                # 非叠加状态，保存当前显示的图像
                if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
                    QMessageBox.warning(self, "警告", "没有可用的图像！")
                    return
                
                current_image = self.images[self.current_index]
                save_pixmap = current_image['pixmap']
                
                # 提取文件名（不含路径和扩展名）作为默认文件名
                filename = current_image['filename']
                default_name = os.path.splitext(os.path.basename(filename))[0]
            
            # 打开文件保存对话框
            options = QFileDialog.Options()
            file_filter = "FITS文件 (*.fits);;JPEG图像 (*.jpg);;PNG图像 (*.png);;GIF图像 (*.gif);;所有文件 (*)"
            filename, selected_filter = QFileDialog.getSaveFileName(
                self, "保存图像", default_name, file_filter, options=options
            )
            
            if not filename:  # 用户取消了操作
                return
            
            # 确保文件有正确的扩展名
            file_ext = ""
            if "fits" in selected_filter:
                if not filename.lower().endswith(".fits"):
                    filename += ".fits"
                file_ext = "fits"
            elif "jpg" in selected_filter:
                if not filename.lower().endswith((".jpg", ".jpeg")):
                    filename += ".jpg"
                file_ext = "jpg"
            elif "png" in selected_filter:
                if not filename.lower().endswith(".png"):
                    filename += ".png"
                file_ext = "png"
            elif "gif" in selected_filter:
                if not filename.lower().endswith(".gif"):
                    filename += ".gif"
                file_ext = "gif"
            
            # 保存图像
            if file_ext == "fits":
                # 保存为FITS文件 (需要转换为numpy数组并使用astropy)
                try:
                    from astropy.io import fits
                    
                    # 将QPixmap转换为QImage然后转换为numpy数组
                    qimage = save_pixmap.toImage()
                    width = qimage.width()
                    height = qimage.height()
                    
                    # 检查图像是灰度还是彩色
                    if qimage.format() == QImage.Format_Grayscale8:
                        # 灰度图像
                        ptr = qimage.bits()
                        ptr.setsize(height * width)
                        data = np.frombuffer(ptr, np.uint8).reshape((height, width))
                    else:
                        # 彩色图像转换为灰度
                        ptr = qimage.bits()
                        ptr.setsize(height * width * 4)  # 4 bytes per pixel (RGBA)
                        data_rgba = np.frombuffer(ptr, np.uint8).reshape((height, width, 4))
                        # 转换为灰度
                        data = np.mean(data_rgba[:, :, :3], axis=2).astype(np.uint8)
                    
                    # 创建FITS头信息
                    hdu = fits.PrimaryHDU(data)
                    hdu.header['DATE'] = datetime.now().strftime('%Y-%m-%d')
                    hdu.header['CREATOR'] = 'GSUN View'
                    
                    # 写入FITS文件
                    hdu.writeto(filename, overwrite=True)
                    
                except ImportError:
                    QMessageBox.critical(self, "错误", "保存FITS文件需要安装astropy库。请安装该库后再试。")
                    return
                except Exception as e:
                    QMessageBox.critical(self, "错误", f"保存FITS文件时出错: {str(e)}")
                    return
            else:
                # 保存为常规图像格式
                if not save_pixmap.save(filename, file_ext.upper()):
                    QMessageBox.critical(self, "错误", f"保存图像失败。请检查文件路径和权限。")
                    return
            
            # 成功保存
            self.statusBar().showMessage(f"图像已保存至 {filename}")
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存图像时出错: {str(e)}")
            import traceback
            traceback.print_exc()

    def toggle_negative(self, checked):
        """切换反色（负片）效果"""
        self.use_negative = checked
        
        # 保存设置
        self.save_config()

    def clean_records_for_removed_images(self, removed_images):
        """清理与删除图像相关的点击记录"""
        if not removed_images or not self.click_records:
            return
            
        # 记录删除前的记录数
        original_count = len(self.click_records)
            
        # 过滤掉与已删除图像相关的记录
        self.click_records = [record for record in self.click_records 
                             if record['filename'] not in removed_images]
        
        # 计算删除的记录数
        removed_count = original_count - len(self.click_records)
        
        # 如果有记录被删除，显示提示（使用弹窗而不是状态栏）
        if removed_count > 0:
            # 移除状态栏消息，使用弹窗提示
            # self.statusBar().showMessage(f"已自动删除 {removed_count} 条与已删除图像相关的点击记录")
            QMessageBox.information(self, "记录清理", f"已自动删除 {removed_count} 条与已删除图像相关的点击记录")
            
        # 如果当前有报告对话框打开，则刷新其内容
        for widget in QApplication.topLevelWidgets():
            if isinstance(widget, QDialog) and widget.windowTitle() == "报告展示":
                for tree_widget in widget.findChildren(QTreeWidget):
                    # 清空记录树
                    tree_widget.clear()
                    # 重新添加记录
                    for record in self.click_records:
                        item = QTreeWidgetItem([
                            record['filename'],
                            record['time'],
                            record['coordinates']
                        ])
                        tree_widget.addTopLevelItem(item)
                break

    def show_image_list_dialog(self):
        """显示图片列表对话框"""
        if not self.images:
            QMessageBox.information(self, "提示", "请先导入图片")
            return
            
        dialog = QDialog(self)
        dialog.setWindowTitle("图片列表")
        dialog.setMinimumSize(500, 400)
        
        layout = QVBoxLayout(dialog)
        
        # 创建图片列表
        list_widget = QListWidget()
        list_widget.setSelectionMode(QListWidget.ExtendedSelection)
        
        # 添加图片到列表
        for i, image in enumerate(self.images):
            filename = os.path.basename(image['filename'])
            item = QListWidgetItem(filename)
            list_widget.addItem(item)
            
        # 选中当前图片
        if self.current_index >= 0 and self.current_index < len(self.images):
            list_widget.setCurrentRow(self.current_index)
            
        layout.addWidget(list_widget)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        select_btn = QPushButton("选择")
        select_btn.clicked.connect(lambda: self.on_image_selected(list_widget.currentRow()))
        button_layout.addWidget(select_btn)
        
        delete_btn = QPushButton("删除选中")
        delete_btn.clicked.connect(lambda: self.remove_selected_images_from_dialog(list_widget))
        button_layout.addWidget(delete_btn)
        
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(dialog.reject)
        button_layout.addWidget(cancel_btn)
        
        layout.addLayout(button_layout)
        
        # 显示对话框
        dialog.exec_()

    def remove_selected_images_from_dialog(self, list_widget):
        """从对话框中删除选中的图片"""
        selected_rows = [item.row() for item in list_widget.selectedIndexes()]
        if not selected_rows:
            return
            
        # 按索引从大到小排序，以便从后往前删除
        selected_rows.sort(reverse=True)
        
        # 确认删除
        reply = QMessageBox.question(
            self, 
            "确认删除", 
            f"确定要删除选中的 {len(selected_rows)} 张图片吗？",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            # 存储要删除的图像文件名
            removed_images = []
            
            # 删除图片
            for row in selected_rows:
                if 0 <= row < len(self.images):
                    # 记录被删除的图像文件名
                    removed_images.append(os.path.basename(self.images[row]['filename']))
                    
                    # 从列表中移除
                    list_widget.takeItem(row)
                    
                    # 从数据中移除
                    del self.images[row]
                    if row < len(self.original_images):
                        del self.original_images[row]
            
            # 更新当前索引
            if self.images:
                self.current_index = min(self.current_index, len(self.images) - 1)
                list_widget.setCurrentRow(self.current_index)
                self.show_current_image()
            else:
                # 清空显示
                self.image_view.scene.clear()
                self.coord_label.setText("X: 0, Y: 0")
                self.filename_label.setText("尚未加载图片")
                self.disable_buttons()
                
            # 清理与删除图像相关的点击记录
            self.clean_records_for_removed_images(removed_images)

    def update_zoom(self, value):
        """更新缩放比例"""
        try:
            # 确保value在有效范围内
            value = max(3, min(100, value))
            
            # 将滑块值转换为实际的缩放比例 (10-100 -> 0.1-10.0)
            zoom_factor = value / 10.0
            
            # 同步更新缩放滑块数值（阻断信号避免循环）
            if self.zoom_slider.value() != value:
                self.zoom_slider.blockSignals(True)
                self.zoom_slider.setValue(int(value))
                self.zoom_slider.blockSignals(False)
            
            # 不再在状态栏显示缩放信息，避免与文件名冲突
            # self.statusBar().showMessage(f"缩放: {zoom_factor:.1f}x", 800)
            
            # 更新图像视图的缩放
            if self.image_view:
                self.image_view.zoom_factor = zoom_factor
                self.image_view.setTransform(QTransform().scale(zoom_factor, zoom_factor))
                self.zoom_label.setText(f'缩放: {zoom_factor:.1f}x')
            
            # 如果在全屏模式下，也更新全屏视图的缩放
            if hasattr(self, 'fullscreen_window') and self.fullscreen_window is not None:
                fullscreen_view = self.fullscreen_window.centralWidget()
                if isinstance(fullscreen_view, ImageGraphicsView):
                    fullscreen_view.zoom_factor = zoom_factor
                    fullscreen_view.setTransform(QTransform().scale(zoom_factor, zoom_factor))
                    # 更新全屏窗口的缩放标签
                    if hasattr(self.fullscreen_window, 'zoom_label'):
                        self.fullscreen_window.zoom_label.setText(f'缩放: {zoom_factor:.1f}x')
        except Exception as e:
            print(f"更新缩放比例时出错: {e}")
            # 不再在状态栏显示错误信息
            # self.statusBar().showMessage(f"更新缩放失败", 800)

    def search_images(self):
        """搜索图像"""
        # 创建搜索选项对话框
        dialog = QDialog(self)
        dialog.setWindowTitle("搜索图像")
        dialog.resize(500, 400)
        
        # 对话框布局
        layout = QVBoxLayout(dialog)
        
        # 搜索类型选择
        search_type_group = QGroupBox("搜索类型")
        search_type_layout = QVBoxLayout(search_type_group)
        
        time_search = QRadioButton("按时间搜索")
        time_search.setChecked(True)
        catalog_search = QRadioButton("从采集表格搜索")
        
        search_type_layout.addWidget(time_search)
        search_type_layout.addWidget(catalog_search)
        
        layout.addWidget(search_type_group)
        
        # 搜索选项
        options_widget = QWidget()
        options_layout = QVBoxLayout(options_widget)
        options_layout.setContentsMargins(0, 0, 0, 0)
        
        # 年份选择
        year_layout = QHBoxLayout()
        year_layout.addWidget(QLabel("年份:"))
        year_combo = QComboBox()
        year_combo.addItem("全部")
        year_layout.addWidget(year_combo)
        
        # 月份选择
        month_layout = QHBoxLayout()
        month_layout.addWidget(QLabel("月份:"))
        month_combo = QComboBox()
        month_combo.addItem("全部")
        month_layout.addWidget(month_combo)
        
        # 加载目录数据
        def load_catalog_data():
            try:
                # 获取表格保存路径
                config = configparser.ConfigParser()
                config.read('config.ini')
                table_path = config.get('Settings', 'table_path', fallback="")
                if not table_path or not os.path.exists(table_path):
                    QMessageBox.warning(dialog, "警告", "表格保存路径未设置或不存在，请在设置中配置")
                    return
                
                # 查找所有CSV文件
                csv_files = glob.glob(os.path.join(table_path, "**/*.csv"), recursive=True)
                if not csv_files:
                    QMessageBox.warning(dialog, "警告", f"在 {table_path} 中未找到CSV文件")
                    return
                
                # 收集所有年份
                years = set()
                
                for csv_file in csv_files:
                    try:
                        df = pd.read_csv(csv_file, encoding="utf-8-sig")
                        if "年份" in df.columns:
                            years.update(df["年份"].astype(str).unique())
                    except Exception as e:
                        print(f"读取CSV文件出错: {str(e)}")
                
                # 添加2020-2030年的年份选项
                for year in range(2020, 2031):
                    years.add(str(year))
                
                # 更新年份下拉框
                year_combo.clear()
                year_combo.addItem("全部")
                for year in sorted(years):
                    year_combo.addItem(str(year))
                
                # 更新月份下拉框
                def update_months():
                    month_combo.clear()
                    month_combo.addItem("全部")
                    
                    selected_year = year_combo.currentText()
                    if selected_year == "全部":
                        # 添加1-12月
                        for month in range(1, 13):
                            month_combo.addItem(f"{month:02d}")
                        return
                    
                    months = set()
                    for csv_file in csv_files:
                        try:
                            df = pd.read_csv(csv_file, encoding="utf-8-sig")
                            if "年份" in df.columns and "月份" in df.columns:
                                year_filter = df["年份"].astype(str) == selected_year
                                months.update(df.loc[year_filter, "月份"].astype(str).unique())
                        except Exception as e:
                            print(f"读取CSV文件出错: {str(e)}")
                    
                    # 如果没有找到月份，添加1-12月
                    if not months:
                        for month in range(1, 13):
                            months.add(f"{month:02d}")
                    
                    for month in sorted(months):
                        month_combo.addItem(str(month))
                
                # 连接年份选择事件
                year_combo.currentIndexChanged.connect(update_months)
                
                # 初始化月份
                update_months()
                
            except Exception as e:
                QMessageBox.warning(dialog, "警告", f"加载目录数据出错: {str(e)}")
        
        # 加载目录数据
        load_catalog_data()
        
        # 图片名搜索
        filename_layout = QHBoxLayout()
        filename_layout.addWidget(QLabel("图片名:"))
        filename_input = QLineEdit()
        filename_input.setPlaceholderText("输入图片名（可选）")
        filename_layout.addWidget(filename_input)
        select_from_list_btn = QPushButton("从列表选择")
        filename_layout.addWidget(select_from_list_btn)
        
        # 从列表选择图片
        def select_from_list():
            if not hasattr(self, 'images') or not self.images:
                QMessageBox.warning(dialog, "警告", "本地图像列表为空")
                return
                
            # 创建图像选择对话框
            select_dialog = QDialog(dialog)
            select_dialog.setWindowTitle("选择图像")
            select_dialog.resize(400, 300)
            
            select_layout = QVBoxLayout(select_dialog)
            
            # 图像列表
            image_list = QListWidget()
            for img_path in self.images:
                item = QListWidgetItem(os.path.basename(img_path['filename']))
                item.setToolTip(img_path['filename'])
                image_list.addItem(item)
                
            select_layout.addWidget(image_list)
            
            # 按钮
            btn_layout = QHBoxLayout()
            ok_btn = QPushButton("确定")
            cancel_btn = QPushButton("取消")
            btn_layout.addWidget(ok_btn)
            btn_layout.addWidget(cancel_btn)
            
            select_layout.addLayout(btn_layout)
            
            # 连接按钮
            ok_btn.clicked.connect(select_dialog.accept)
            cancel_btn.clicked.connect(select_dialog.reject)
            
            # 显示对话框
            if select_dialog.exec_() == QDialog.Accepted and image_list.currentItem():
                # 获取选中的图像名
                selected_filename = image_list.currentItem().text()
                # 设置到输入框
                filename_input.setText(selected_filename)
        
        # 连接从列表选择按钮
        select_from_list_btn.clicked.connect(select_from_list)
        
        options_layout.addLayout(year_layout)
        options_layout.addLayout(month_layout)
        options_layout.addLayout(filename_layout)
        
        layout.addWidget(options_widget)
        
        # 搜索结果区域
        result_group = QGroupBox("搜索结果")
        result_layout = QVBoxLayout(result_group)
        
        result_list = QListWidget()
        result_layout.addWidget(result_list)
        
        layout.addWidget(result_group)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        search_button = QPushButton("搜索")
        button_layout.addWidget(search_button)
        
        cancel_button = QPushButton("取消")
        button_layout.addWidget(cancel_button)
        
        layout.addLayout(button_layout)
        
        # 切换搜索类型时的UI更新
        def toggle_widgets():
            # 按时间搜索时，显示年份、月份选择和图片名输入框
            if time_search.isChecked():
                # 显示年份和月份选择
                for widget in year_layout.itemAt(0).widget(), year_layout.itemAt(1).widget():
                    if widget:
                        widget.setVisible(True)
                for widget in month_layout.itemAt(0).widget(), month_layout.itemAt(1).widget():
                    if widget:
                        widget.setVisible(True)
                        
                # 显示图片名搜索
                for widget in filename_layout.itemAt(0).widget(), filename_layout.itemAt(1).widget(), filename_layout.itemAt(2).widget():
                    if widget:
                        widget.setVisible(True)
                        
            elif catalog_search.isChecked():
                # 从采集表格搜索时，只显示图片名输入框
                # 隐藏年份和月份选择
                for widget in year_layout.itemAt(0).widget(), year_layout.itemAt(1).widget():
                    if widget:
                        widget.setVisible(False)
                for widget in month_layout.itemAt(0).widget(), month_layout.itemAt(1).widget():
                    if widget:
                        widget.setVisible(False)
                        
                # 显示图片名搜索
                for widget in filename_layout.itemAt(0).widget(), filename_layout.itemAt(1).widget(), filename_layout.itemAt(2).widget():
                    if widget:
                        widget.setVisible(True)
        
        # 连接搜索类型切换事件
        time_search.toggled.connect(toggle_widgets)
        catalog_search.toggled.connect(toggle_widgets)
        
        # 初始化UI状态
        toggle_widgets()
        
        # 连接搜索按钮
        def on_search_clicked():
            # 获取搜索类型
            search_type = "time" if time_search.isChecked() else "catalog"
            
            # 获取搜索条件
            year = year_combo.currentText()
            month = month_combo.currentText()
            filename = filename_input.text().strip()
            
            # 执行搜索
            self.perform_image_search(search_type, year, month, filename, result_list)
            
            # 关闭对话框
            dialog.accept()
        
        search_button.clicked.connect(on_search_clicked)
        
        # 连接取消按钮
        cancel_button.clicked.connect(dialog.reject)
        
        # 显示对话框
        dialog.exec_()
        
    def on_images_found(self, images):
        """处理找到的图像"""
        self.found_online_images = images
        
        # 更新状态栏
        if len(images) > 0:
            self.statusBar().showMessage(f"找到 {len(images)} 张相关图像")
            
            # 显示结果对话框
            result_dialog = QDialog(self)
            result_dialog.setWindowTitle("搜索结果")
            result_dialog.resize(600, 400)
            
            # 创建布局
            layout = QVBoxLayout(result_dialog)
            
            # 创建表格
            result_table = QTableWidget()
            result_table.setColumnCount(2)
            result_table.setHorizontalHeaderLabels(["图片名", "时间"])
            result_table.horizontalHeader().setStretchLastSection(True)
            result_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive)
            result_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Interactive)
            
            # 启用多选和扩展选择模式
            result_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
            result_table.setSelectionBehavior(QAbstractItemView.SelectItems)
            
            # 添加数据
            for filename, url, date_info in images:
                row = result_table.rowCount()
                result_table.insertRow(row)
                
                # 图片名
                name_item = QTableWidgetItem(filename)
                name_item.setToolTip(url)
                result_table.setItem(row, 0, name_item)
                
                # 时间信息
                date_item = QTableWidgetItem(date_info)
                result_table.setItem(row, 1, date_item)
            
            layout.addWidget(result_table)
            
            # 添加按钮
            button_layout = QHBoxLayout()
            
            # 添加下载按钮
            download_button = QPushButton("下载选中")
            download_button.clicked.connect(lambda: self.download_selected_images(result_table, images))
            button_layout.addWidget(download_button)
            
            # 添加复制按钮
            copy_button = QPushButton("复制选中行")
            copy_button.clicked.connect(lambda: self.copy_selected_items(result_table))
            button_layout.addWidget(copy_button)
            
            close_button = QPushButton("关闭")
            close_button.clicked.connect(result_dialog.accept)
            button_layout.addWidget(close_button)
            layout.addLayout(button_layout)
            
            # 显示对话框
            result_dialog.exec_()
            
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "搜索图像", 
                f"找到 {len(images)} 张相关图像"
            )
            self.update_history_display(history_record)
        else:
            self.statusBar().showMessage("未找到相关图像")
            QMessageBox.information(self, "搜索结果", "未找到相关图像，请尝试其他搜索条件。")
    
    def on_search_error(self, error_msg):
        """处理搜索错误"""
        QMessageBox.warning(self, "搜索错误", error_msg)
        self.statusBar().showMessage("搜索失败")
        
        # 添加到历史记录
        history_record = self.history_logger.add_record(
            "搜索图像", 
            f"搜索失败: {error_msg}"
        )
        self.update_history_display(history_record)
    
    def on_search_progress(self, progress_msg):
        """处理搜索进度更新"""
        self.statusBar().showMessage(progress_msg)
        
    def copy_selected_items(self, table):
        """复制选中行的所有信息到剪贴板"""
        selected_items = table.selectedItems()
        if not selected_items:
            QMessageBox.information(self, "提示", "请先选择要复制的内容")
            return
            
        # 获取选中的行
        selected_rows = set(item.row() for item in selected_items)
        
        # 构建复制文本
        clipboard_text = ""
        
        # 获取表头
        headers = []
        for col in range(table.columnCount()):
            header_item = table.horizontalHeaderItem(col)
            if header_item:
                headers.append(header_item.text())
            else:
                headers.append(f"列{col+1}")
        
        # 按行复制所有列的信息
        for row in selected_rows:
            row_text = []
            for col in range(table.columnCount()):
                item = table.item(row, col)
                value = item.text() if item else ""
                row_text.append(f"{headers[col]}: {value}")
            
            # 将该行所有信息添加到复制文本中
            clipboard_text += "\n".join(row_text) + "\n\n"
        
        # 复制到剪贴板
        clipboard = QApplication.clipboard()
        clipboard.setText(clipboard_text)
        
        QMessageBox.information(self, "成功", "已复制选中行的所有信息到剪贴板")
        
    def download_selected_images(self, table, images):
        """下载选中的图像"""
        # 获取选中的行
        selected_rows = set(index.row() for index in table.selectedIndexes())
        if not selected_rows:
            QMessageBox.information(self, "提示", "请先选择要下载的图像")
            return
            
        # 获取下载路径
        config = configparser.ConfigParser()
        config.read('config.ini')
        download_path = config.get('Settings', 'download_path', fallback="")
        
        if not download_path:
            # 如果没有配置下载路径，提示用户选择
            download_path = QFileDialog.getExistingDirectory(self, "选择下载目录")
            if not download_path:
                return
                
            # 保存到配置文件
            config.set('Settings', 'download_path', download_path)
            with open('config.ini', 'w') as f:
                config.write(f)
        
        # 确保下载目录存在
        if not os.path.exists(download_path):
            try:
                os.makedirs(download_path)
            except Exception as e:
                QMessageBox.warning(self, "错误", f"创建下载目录失败: {str(e)}")
                return
        
        # 创建进度对话框
        progress_dialog = QProgressDialog("正在下载图像...", "取消", 0, len(selected_rows), self)
        progress_dialog.setWindowTitle("下载进度")
        progress_dialog.setWindowModality(Qt.WindowModal)
        progress_dialog.setMinimumDuration(0)
        progress_dialog.setValue(0)
        
        # 下载选中的图像
        success_count = 0
        failed_count = 0
        
        for i, row in enumerate(sorted(selected_rows)):
            if progress_dialog.wasCanceled():
                break
                
            filename, url, _ = images[row]
            progress_dialog.setLabelText(f"正在下载: {filename}")
            
            try:
                # 下载图像
                response = requests.get(url, timeout=30)
                if response.status_code == 200:
                    # 保存图像
                    file_path = os.path.join(download_path, filename)
                    with open(file_path, 'wb') as f:
                        f.write(response.content)
                    success_count += 1
                else:
                    failed_count += 1
            except Exception as e:
                print(f"下载图像出错: {str(e)}")
                failed_count += 1
            
            progress_dialog.setValue(i + 1)
        
        # 关闭进度对话框
        progress_dialog.close()
        
        # 显示下载结果
        if success_count > 0:
            QMessageBox.information(self, "下载完成", f"成功下载 {success_count} 张图像到 {download_path}" + 
                                   (f"\n{failed_count} 张图像下载失败" if failed_count > 0 else ""))
            
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "下载图像", 
                f"下载了 {success_count} 张图像" + (f", {failed_count} 张失败" if failed_count > 0 else "")
            )
            self.update_history_display(history_record)
        else:
            QMessageBox.warning(self, "下载失败", "所有图像下载失败，请检查网络连接或图像URL是否有效")
        
    def perform_image_search(self, search_type, year, month, filename, result_list):
        """执行图像搜索"""
        # 获取表格路径
        config = configparser.ConfigParser()
        config.read('config.ini')
        table_path = config.get('Settings', 'table_path', fallback="")
        if not table_path:
            QMessageBox.warning(self, "警告", "请先在设置中配置表格保存路径")
            return
            
        if not os.path.exists(table_path):
            QMessageBox.warning(self, "警告", f"表格保存路径不存在: {table_path}")
            return
        
        # 显示正在搜索的提示
        self.statusBar().showMessage(f"正在搜索...")
        
        # 清空结果列表
        result_list.clear()
        
        # 构建搜索条件
        search_direct_url = ""
        if search_type == "time":
            if year == "全部":
                QMessageBox.warning(self, "警告", "请至少选择一个年份")
                return
            search_direct_url = f"{year}{month}"
        elif search_type == "catalog":
            search_direct_url = "catalog"
        
        # 创建并启动搜索线程
        self.search_thread = OnlineImageSearchThread(table_path, search_direct_url, search_type)
        
        if search_type == "time":
            self.search_thread.year = year
            self.search_thread.month = month
            # 如果有图片名，也传递给搜索线程
            if filename:
                self.search_thread.filename = filename
        elif search_type == "catalog":
            # 如果有图片名，传递给搜索线程
            if filename:
                self.search_thread.filename = filename
        
        # 连接信号
        self.search_thread.found_signal.connect(self.on_images_found)
        self.search_thread.error_signal.connect(self.on_search_error)
        self.search_thread.progress_signal.connect(self.on_search_progress)
        
        # 启动线程
        self.search_thread.start()
    
    def show_screen_stretch(self):
        """显示Screen Stretch对话框，用于调节FITS图像的显示效果"""
        if not self.images or self.current_index < 0 or self.current_index >= len(self.images):
            QMessageBox.warning(self, "警告", "请先加载FITS图像！")
            return
            
        # 获取当前图像信息
        current_image = self.images[self.current_index]
        file_path = current_image['filename']
        
        # 检查文件格式
        if not file_path.lower().endswith(('.fits', '.fts', '.fit')):
            QMessageBox.warning(self, "警告", "只能对FITS格式的图像进行Screen Stretch调节！")
            return
            
        # 初始化或重新加载image_processor
        if not hasattr(self, 'image_processor') or self.image_processor is None:
            self.image_processor = FitsImageProcessor()
        
        # 加载当前图像到image_processor
        try:
            load_result = self.image_processor.load_fits(file_path)
            if not load_result or self.image_processor.image_data is None:
                QMessageBox.warning(self, "警告", "无法加载FITS图像数据！")
                return
                
            # 创建Screen Stretch对话框
            from screen_stretch import ScreenStretchDialog
            dialog = ScreenStretchDialog(self, self.image_processor.image_data)
            
            # 连接信号
            dialog.stretchUpdated.connect(self.apply_screen_stretch)
            
            # 显示对话框
            dialog.exec_()
            
        except Exception as e:
            QMessageBox.warning(self, "警告", f"加载图像时出错: {str(e)}")
            return
            
    def auto_histogram(self):
        """执行自动直方图功能 - 对所有已导入的FITS图像生效"""
        if not self.images:
            QMessageBox.warning(self, "警告", "请先加载FITS图像！")
            return
        
        # 筛选出FITS格式的图像
        fits_images = []
        for i, image in enumerate(self.images):
            file_path = image['filename']
            if file_path.lower().endswith(('.fits', '.fts', '.fit')):
                fits_images.append((i, image))
        
        if not fits_images:
            QMessageBox.warning(self, "警告", "没有找到FITS格式的图像！")
            return
        
        # 显示进度对话框
        progress = QProgressDialog("正在对所有FITS图像应用自动直方图...", "取消", 0, len(fits_images), self)
        progress.setWindowTitle("批量自动直方图处理")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        # 初始化或重新加载image_processor
        if not hasattr(self, 'image_processor') or self.image_processor is None:
            self.image_processor = FitsImageProcessor()
        
        processed_count = 0
        
        # 第一步：收集所有图像数据并计算统一的直方图参数
        progress.setLabelText("正在计算统一的直方图参数...")
        QApplication.processEvents()  # 保持UI响应
        
        all_image_data = []
        valid_image_indices = []
        
        # 首先加载所有图像数据
        for idx, (image_index, image) in enumerate(fits_images):
            if progress.wasCanceled():
                break
                
            progress.setValue(idx)
            progress.setLabelText(f"正在加载第 {idx+1}/{len(fits_images)} 张图像: {os.path.basename(image['filename'])}")
            QApplication.processEvents()  # 保持UI响应
            
            file_path = image['filename']
            
            try:
                # 加载图像到image_processor
                load_result = self.image_processor.load_fits(file_path)
                if not load_result or self.image_processor.image_data is None:
                    print(f"无法加载FITS图像数据: {file_path}")
                    continue
                    
                # 收集图像数据
                all_image_data.append(self.image_processor.image_data.copy())
                valid_image_indices.append(image_index)
                
            except Exception as e:
                print(f"加载图像 {file_path} 时出错: {e}")
                continue
        
        # 如果没有成功加载任何图像，则退出
        if not all_image_data:
            progress.close()
            QMessageBox.warning(self, "警告", "没有成功加载任何图像！")
            return
        
        # 计算统一的直方图参数
        try:
            # 将所有图像数据合并为一个大数组
            combined_data = np.concatenate([data.flatten() for data in all_image_data])
            
            # 创建Screen Stretch对话框并计算统一参数
            from screen_stretch import ScreenStretchDialog
            stretch_dialog = ScreenStretchDialog(self, combined_data.reshape(-1, 1))
            stretch_dialog.auto_histogram()
            
            # 获取统一的直方图参数
            min_value = stretch_dialog.min_value
            max_value = stretch_dialog.max_value
            
            print(f"计算得到统一的直方图参数: min_value={min_value}, max_value={max_value}")
            
        except Exception as e:
            progress.close()
            QMessageBox.warning(self, "警告", f"计算统一直方图参数时出错: {str(e)}")
            return
        
        # 第二步：应用统一参数到所有图像
        for idx, (image_index, image_data) in enumerate(zip(valid_image_indices, all_image_data)):
            if progress.wasCanceled():
                break
                
            progress.setValue(idx)
            file_path = self.images[image_index]['filename']
            progress.setLabelText(f"正在处理第 {idx+1}/{len(valid_image_indices)} 张图像: {os.path.basename(file_path)}")
            QApplication.processEvents()  # 保持UI响应
            
            try:
                # 应用统一的拉伸参数到当前图像
                stretched_data = np.clip(image_data, min_value, max_value)
                normalized_data = ((stretched_data - min_value) / (max_value - min_value) * 255).astype(np.uint8)
                
                # 转换为QPixmap
                height, width = normalized_data.shape
                qimage = QImage(normalized_data.data, width, height, width, QImage.Format_Grayscale8)
                pixmap = QPixmap.fromImage(qimage)
                
                # 更新图像列表中的pixmap
                self.images[image_index]['pixmap'] = pixmap
                
                processed_count += 1
                    
            except Exception as e:
                print(f"处理图像 {file_path} 时出错: {e}")
                continue
        
        progress.setValue(len(fits_images))
        
        # 刷新当前显示的图像
        self.show_current_image()
        
        # 添加到历史记录
        history_record = self.history_logger.add_record(
            "批量自动直方图", 
            f"已对 {processed_count} 张FITS图像应用统一的自动直方图处理 (min={min_value:.2f}, max={max_value:.2f})"
        )
        self.update_history_display(history_record)
        
        # 显示完成消息
        if processed_count > 0:
            QMessageBox.information(self, "完成", f"已成功对 {processed_count} 张FITS图像应用统一的自动直方图处理！\n统一参数: min={min_value:.2f}, max={max_value:.2f}")
            self.statusBar().showMessage(f"已对 {processed_count} 张FITS图像应用统一的自动直方图处理")
        else:
            QMessageBox.warning(self, "警告", "没有成功处理任何图像！")
    
    def apply_screen_stretch(self, min_value, max_value):
        """应用Screen Stretch设置到当前FITS图像"""
        if not hasattr(self, 'image_processor') or self.image_processor is None:
            return
            
        try:
            # 获取图像数据
            image_data = self.image_processor.image_data
            if image_data is None:
                return
                
            # 应用拉伸
            stretched_data = np.clip(image_data, min_value, max_value)
            normalized_data = ((stretched_data - min_value) / (max_value - min_value) * 255).astype(np.uint8)
            
            # 转换为QPixmap
            height, width = normalized_data.shape
            qimage = QImage(normalized_data.data, width, height, width, QImage.Format_Grayscale8)
            pixmap = QPixmap.fromImage(qimage)
            
            # 更新当前图像
            if 0 <= self.current_index < len(self.images):
                self.images[self.current_index]['pixmap'] = pixmap
            self.show_current_image()
            
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "Screen Stretch", 
                f"应用Screen Stretch: 最小值={min_value}, 最大值={max_value}"
            )
            self.update_history_display(history_record)
            
            # 保存当前的Screen Stretch设置到配置文件
            try:
                config_file = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "screen_stretch_config.json"))
                
                # 计算当前图像数据的百分比值
                if image_data is not None:
                    flat_data = image_data.flatten()
                    # 反向计算百分比值（近似值）
                    # 找到min_value和max_value在数据中的百分位数
                    sorted_data = np.sort(flat_data)
                    total_pixels = len(sorted_data)
                    
                    # 找到最接近min_value的位置
                    min_idx = np.searchsorted(sorted_data, min_value)
                    min_percent = (min_idx / total_pixels) * 100
                    
                    # 找到最接近max_value的位置
                    max_idx = np.searchsorted(sorted_data, max_value)
                    max_percent = (max_idx / total_pixels) * 100
                    
                    # 确保百分比在有效范围内
                    min_percent = max(0, min(min_percent, 100))
                    max_percent = max(0, min(max_percent, 100))
                    
                    print(f"计算得到的百分比值: min_percent={min_percent}, max_percent={max_percent}")
                    
                    # 读取现有配置以获取stretch_mode
                    stretch_mode = "Medium"  # 默认值
                    if os.path.exists(config_file):
                        with open(config_file, "r") as f:
                            existing_config = json.load(f)
                            stretch_mode = existing_config.get("stretch_mode", "Medium")
                    
                    # 创建新的配置
                    new_config = {
                        "min_percent": min_percent,
                        "max_percent": max_percent,
                        "stretch_mode": stretch_mode
                    }
                    
                    # 保存配置
                    with open(config_file, "w") as f:
                        json.dump(new_config, f, indent=4)
                    
                    print(f"已更新Screen Stretch配置: min_percent={min_percent}, max_percent={max_percent}, mode={stretch_mode}")
                else:
                    print("无法更新配置：图像数据为空")
            except Exception as e:
                print(f"更新Screen Stretch配置文件时出错: {e}")
                import traceback
                traceback.print_exc()
            
        except Exception as e:
            print(f"应用Screen Stretch时出错: {e}")
            import traceback
            traceback.print_exc()
            
    def show_image_settings(self):
        """显示图片设置对话框"""
        dialog = QDialog(self)
        dialog.setWindowTitle("图片设置")
        dialog.setFixedSize(400, 360)  # 增加高度以容纳重置按钮
        
        layout = QVBoxLayout()
        dialog.setLayout(layout)
        
        # 亮度调整
        layout.addWidget(QLabel("- 亮度调整 -"))
        layout.addWidget(QLabel("调整所有图片的亮度值"))
        
        brightness_layout = QHBoxLayout()
        brightness_layout.addWidget(QLabel("亮度:"))
        
        # 全局亮度设置
        if not hasattr(self, 'global_brightness_value'):
            self.global_brightness_value = 110  # 默认110%
            
        brightness_value_label = QLabel(f"{self.global_brightness_value}%")
        
        brightness_slider = QSlider(Qt.Horizontal)
        brightness_slider.setRange(10, 200)  # 10% - 200%
        brightness_slider.setValue(self.global_brightness_value)
        brightness_slider.valueChanged.connect(lambda value: self.update_global_brightness_value(value, brightness_value_label))
        
        brightness_layout.addWidget(brightness_slider)
        brightness_layout.addWidget(brightness_value_label)
        layout.addLayout(brightness_layout)
        
        # 对比度调整
        layout.addWidget(QLabel("- 对比度调整 -"))
        layout.addWidget(QLabel("调整所有图片的对比度"))
        
        contrast_layout = QHBoxLayout()
        contrast_layout.addWidget(QLabel("对比度:"))
        
        # 全局对比度设置
        if not hasattr(self, 'global_contrast_value'):
            self.global_contrast_value = 100  # 默认100%
            
        contrast_value_label = QLabel(f"{self.global_contrast_value}%")
        
        contrast_slider = QSlider(Qt.Horizontal)
        contrast_slider.setRange(10, 200)  # 10% - 200%
        contrast_slider.setValue(self.global_contrast_value)
        contrast_slider.valueChanged.connect(lambda value: self.update_global_contrast_value(value, contrast_value_label))
        
        contrast_layout.addWidget(contrast_slider)
        contrast_layout.addWidget(contrast_value_label)
        layout.addLayout(contrast_layout)
        
        # 伽马值调整
        layout.addWidget(QLabel("- 伽马值调整 -"))
        layout.addWidget(QLabel("调整所有图片的伽马值，影响中间调"))
        
        gamma_layout = QHBoxLayout()
        gamma_layout.addWidget(QLabel("伽马值:"))
        
        # 全局伽马值设置
        if not hasattr(self, 'global_gamma_value'):
            self.global_gamma_value = 1.0  # 默认1.0
            
        gamma_value_label = QLabel(f"{self.global_gamma_value:.1f}")
        
        gamma_slider = QSlider(Qt.Horizontal)
        gamma_slider.setRange(5, 20)  # 0.5 - 2.0
        gamma_slider.setValue(int(self.global_gamma_value * 10))
        gamma_slider.valueChanged.connect(lambda value: self.update_global_gamma_value(value, gamma_value_label))
        
        gamma_layout.addWidget(gamma_slider)
        gamma_layout.addWidget(gamma_value_label)
        layout.addLayout(gamma_layout)
        
        # 重置按钮
        reset_button = QPushButton("重置为默认值")
        reset_button.clicked.connect(lambda: self.reset_image_settings(
            brightness_slider, brightness_value_label,
            contrast_slider, contrast_value_label,
            gamma_slider, gamma_value_label
        ))
        layout.addWidget(reset_button)
        
        # 应用设置提示
        layout.addWidget(QLabel("注意：设置将应用于所有图片"))
        
        # 按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(dialog.accept)
        button_box.rejected.connect(dialog.reject)
        layout.addWidget(button_box)
        
        # 显示对话框
        if dialog.exec_() == QDialog.Accepted:
            self.apply_global_settings()
            
    def reset_image_settings(self, brightness_slider, brightness_label, contrast_slider, contrast_label, gamma_slider, gamma_label):
        """重置图片设置为默认值"""
        # 重置亮度
        self.global_brightness_value = 110
        brightness_slider.setValue(110)
        brightness_label.setText("110%")
        
        # 重置对比度
        self.global_contrast_value = 100
        contrast_slider.setValue(100)
        contrast_label.setText("100%")
        
        # 重置伽马值
        self.global_gamma_value = 1.0
        gamma_slider.setValue(10)
        gamma_label.setText("1.0")
    
    def update_global_brightness_value(self, value, label):
        """更新全局亮度值"""
        self.global_brightness_value = value
        label.setText(f"{value}%")
        
    def update_global_contrast_value(self, value, label):
        """更新全局对比度值"""
        self.global_contrast_value = value
        label.setText(f"{value}%")
        
    def update_global_gamma_value(self, value, label):
        """更新全局伽马值"""
        self.global_gamma_value = value / 10.0  # 将滑块值转换为0.5-2.0范围
        label.setText(f"{self.global_gamma_value:.1f}")
    
    def collect_catalog(self):
        """采集目录数据并生成表格"""
        # 显示采集对话框
        dialog = CatalogCollectionDialog(self)
        if dialog.exec_() != QDialog.Accepted:
            return
            
        # 获取选中的月份
        selected_months = dialog.get_selected_months()
        if not selected_months:
            QMessageBox.warning(self, "警告", "请至少选择一个月份")
            return
            
        # 询问是否覆盖现有表格
        config = configparser.ConfigParser()
        config.read('config.ini')
        table_path = config.get('Settings', 'table_path', fallback="")
        if os.path.exists(os.path.join(table_path, "catalog.csv")):
            reply = QMessageBox.question(
                self, 
                "确认覆盖", 
                "已存在采集表格，是否覆盖？",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            if reply != QMessageBox.Yes:
                return
        
        # 创建进度对话框
        progress_dialog = QDialog(self)
        progress_dialog.setWindowTitle("采集进度")
        progress_dialog.setWindowModality(Qt.WindowModal)
        progress_dialog.resize(500, 400)
        
        # 创建对话框布局
        dialog_layout = QVBoxLayout(progress_dialog)
        
        # 创建日志文本框
        log_text = QTextEdit()
        log_text.setReadOnly(True)
        dialog_layout.addWidget(log_text)
        
        # 创建进度条
        progress_bar = QProgressBar()
        progress_bar.setRange(0, 0)  # 设置为不确定模式
        dialog_layout.addWidget(progress_bar)
        
        # 创建取消按钮
        cancel_button = QPushButton("取消")
        cancel_button.clicked.connect(progress_dialog.reject)
        dialog_layout.addWidget(cancel_button)
        
        # 创建并启动采集线程
        base_url = "https://download.china-vo.org/"
        self.catalog_thread = CatalogCollectionThread(base_url, selected_months)
        
        # 连接信号
        self.catalog_thread.progress_signal.connect(lambda msg: self._update_catalog_progress(msg, log_text))
        self.catalog_thread.completed_signal.connect(lambda df: self._on_catalog_completed(df, progress_dialog, progress_bar))
        self.catalog_thread.error_signal.connect(lambda err: self._on_catalog_error(err, progress_dialog))
        
        # 连接取消按钮
        cancel_button.clicked.connect(self.catalog_thread.cancel)
        
        # 启动线程
        self.catalog_thread.start()
        
        # 显示进度对话框
        progress_dialog.exec_()
    
    def _update_catalog_progress(self, message, text_edit):
        """更新采集进度"""
        text_edit.append(message)
        # 滚动到底部
        text_edit.verticalScrollBar().setValue(text_edit.verticalScrollBar().maximum())
    
    def _on_catalog_completed(self, df, progress_dialog, progress_bar):
        """采集完成处理"""
        # 保存表格
        try:
            # 确保目录存在
            config = configparser.ConfigParser()
            config.read('config.ini')
            table_path = config.get('Settings', 'table_path', fallback="")
            if not table_path:
                table_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "catalogs")
            
            os.makedirs(table_path, exist_ok=True)
            
            # 检查数据是否为空
            if df.empty:
                text_edit = None
                for child in progress_dialog.findChildren(QTextEdit):
                    text_edit = child
                    break
                    
                if text_edit:
                    self._update_catalog_progress(f"采集完成，但未找到任何数据", text_edit)
                
                # 修改进度条
                progress_bar.setRange(0, 100)
                progress_bar.setValue(100)
                
                # 修改对话框按钮
                for child in progress_dialog.findChildren(QPushButton):
                    child.setText("关闭")
                
                return
            
            # 获取选中的月份，创建月份文件夹
            monthly_files = {}
            for _, row in df.iterrows():
                year = row['年份']
                month = row['月份']
                folder_name = f"{year}{month}"  # 例如 202501
                
                if folder_name not in monthly_files:
                    # 创建月份文件夹
                    month_path = os.path.join(table_path, folder_name)
                    os.makedirs(month_path, exist_ok=True)
                    monthly_files[folder_name] = []
                
                monthly_files[folder_name].append(row)
            
            # 保存月份表格
            saved_files = []
            for folder_name, rows in monthly_files.items():
                month_df = pd.DataFrame(rows)
                month_path = os.path.join(table_path, folder_name)
                month_csv_path = os.path.join(month_path, f"{folder_name}.csv")
                month_df.to_csv(month_csv_path, index=False, encoding="utf-8-sig")
                saved_files.append(month_csv_path)
            
            # 更新进度对话框
            text_edit = None
            for child in progress_dialog.findChildren(QTextEdit):
                text_edit = child
                break
                
            if text_edit:
                self._update_catalog_progress(f"采集完成，共 {len(df)} 条数据", text_edit)
                self._update_catalog_progress(f"数据已保存到：", text_edit)
                for file_path in saved_files:
                    self._update_catalog_progress(f"- {file_path}", text_edit)
            
            # 修改进度条
            progress_bar.setRange(0, 100)
            progress_bar.setValue(100)
            
            # 修改对话框按钮
            for child in progress_dialog.findChildren(QPushButton):
                child.setText("关闭")
                
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "采集目录", 
                f"采集完成，共 {len(df)} 条数据，保存到 {len(saved_files)} 个文件"
            )
            self.update_history_display(history_record)
            
        except Exception as e:
            text_edit = None
            for child in progress_dialog.findChildren(QTextEdit):
                text_edit = child
                break
                
            if text_edit:
                self._update_catalog_progress(f"保存数据时出错: {str(e)}", text_edit)
                import traceback
                self._update_catalog_progress(f"错误详情: {traceback.format_exc()}", text_edit)
    
    def _on_catalog_error(self, error_msg, progress_dialog):
        """采集错误处理"""
        text_edit = None
        for child in progress_dialog.findChildren(QTextEdit):
            text_edit = child
            break
            
        if text_edit:
            self._update_catalog_progress(f"错误: {error_msg}", text_edit)
        
        # 修改对话框按钮
        for child in progress_dialog.findChildren(QPushButton):
            child.setText("关闭")
        
    def apply_global_settings(self):
        """将全局设置应用于所有图片"""
        if not self.images:
            QMessageBox.warning(self, "警告", "没有加载图像，无法应用设置！")
            return
            
        # 显示进度对话框
        progress = QProgressDialog("正在应用图片设置...", "取消", 0, len(self.images), self)
        progress.setWindowTitle("处理中")
        progress.setWindowModality(Qt.WindowModal)
        progress.show()
        
        try:
            # 重新处理所有原始图像
            for i, original_data in enumerate(self.original_images):
                if progress.wasCanceled():
                    break
                    
                # 更新进度
                progress.setValue(i)
                
                # 应用设置
                processed_data = self.apply_image_settings(original_data)
                
                # 更新图像
                height, width = processed_data.shape[:2]
                if len(processed_data.shape) == 2:  # 灰度图像
                    qimage = QImage(processed_data.data, width, height, width, QImage.Format_Grayscale8)
                else:  # 彩色图像
                    bytes_per_line = 3 * width
                    qimage = QImage(processed_data.data, width, height, bytes_per_line, QImage.Format_RGB888)
                
                pixmap = QPixmap.fromImage(qimage)
                self.images[i]['pixmap'] = pixmap
                
            # 显示当前图像
            if self.current_index >= 0 and self.current_index < len(self.images):
                self.show_current_image()
                
            # 如果处于叠加模式，更新叠加图像
            if self.is_stacked:
                self.stacked_image = self.create_stacked_image()
                if self.stacked_image is not None:
                    stack_info = f"叠加图像 [亮度:{self.brightness_value}% 对比度:{self.contrast_value}%]"
                    self.image_view.set_image(self.stacked_image, filename=stack_info)
                
            # 添加到历史记录
            history_record = self.history_logger.add_record(
                "图片设置", 
                f"应用全局图片设置：亮度 {self.global_brightness_value}%，对比度 {self.global_contrast_value}%，伽马值 {self.global_gamma_value:.1f}"
            )
            self.update_history_display(history_record)
            
            QMessageBox.information(self, "完成", "图片设置已应用于所有图片！")
            
        except Exception as e:
            QMessageBox.critical(self, "错误", f"应用图片设置时出错: {str(e)}")
            import traceback
            traceback.print_exc()
        finally:
            progress.close()
    
    def apply_image_settings(self, image_data):
        """应用图片设置到单张图片"""
        try:
            # 创建图像副本避免修改原始数据
            processed = image_data.copy().astype(np.float32)
            
            # 应用伽马校正
            if self.global_gamma_value != 1.0:
                # 将数据归一化到0-1
                if processed.max() > 0:
                    normalized = processed / 255.0
                    # 应用伽马校正
                    epsilon = 1e-5  # 避免对零进行幂操作
                    processed = np.power(normalized + epsilon, self.global_gamma_value) * 255.0
            
            # 应用亮度
            brightness_factor = self.global_brightness_value / 100.0
            processed = processed * brightness_factor
            
            # 应用对比度
            if self.global_contrast_value != 100:
                contrast_factor = self.global_contrast_value / 100.0
                mean_value = np.mean(processed)
                processed = (processed - mean_value) * contrast_factor + mean_value
            
            # 裁剪数值到0-255范围
            processed = np.clip(processed, 0, 255).astype(np.uint8)
            
            return processed
            
        except Exception as e:
            print(f"应用图片设置时出错: {e}")
            import traceback
            traceback.print_exc()
            return image_data

    def horizontal_flip(self):
        """水平翻转所有图片"""
        if not self.images:
            return
            
        try:
            # 遍历所有图像进行水平翻转
            for i, image in enumerate(self.images):
                current_pixmap = image['pixmap']
                # 创建水平翻转的QPixmap
                flipped_pixmap = current_pixmap.transformed(QTransform().scale(-1, 1))
                # 更新图像
                image['pixmap'] = flipped_pixmap
                
                # 同时更新original_images中的数据（如果存在）
                if i < len(self.original_images):
                    try:
                        # 获取原始图像数据
                        original_data = self.original_images[i]
                        if original_data is not None and isinstance(original_data, np.ndarray):
                            # 水平翻转numpy数组
                            flipped_data = np.flip(original_data, axis=1)
                            # 更新原始图像数据
                            self.original_images[i] = flipped_data
                    except Exception as e:
                        print(f"水平翻转original_images[{i}]时出错: {e}")
                        # 继续处理其他图像，不中断整个过程
            
            # 重新显示当前图像
            self.show_current_image()
            
            # 记录历史
            history_record = self.history_logger.add_record(
                "批量处理", 
                f"已对所有 {len(self.images)} 张图像进行水平翻转"
            )
            self.update_history_display(history_record)
            
            # 显示状态栏消息
            self.statusBar().showMessage(f"已对所有 {len(self.images)} 张图像进行水平翻转")
            
        except Exception as e:
            print(f"水平翻转图像时出错: {e}")
            import traceback
            traceback.print_exc()  # 打印详细错误信息
            self.statusBar().showMessage(f"水平翻转失败: {e}")
            
    def vertical_flip(self):
        """垂直翻转所有图片"""
        if not self.images:
            return
            
        try:
            # 遍历所有图像进行垂直翻转
            for i, image in enumerate(self.images):
                current_pixmap = image['pixmap']
                # 创建垂直翻转的QPixmap
                flipped_pixmap = current_pixmap.transformed(QTransform().scale(1, -1))
                # 更新图像
                image['pixmap'] = flipped_pixmap
                
                # 同时更新original_images中的数据（如果存在）
                if i < len(self.original_images):
                    try:
                        # 获取原始图像数据
                        original_data = self.original_images[i]
                        if original_data is not None and isinstance(original_data, np.ndarray):
                            # 垂直翻转numpy数组
                            flipped_data = np.flip(original_data, axis=0)
                            # 更新原始图像数据
                            self.original_images[i] = flipped_data
                    except Exception as e:
                        print(f"垂直翻转original_images[{i}]时出错: {e}")
                        # 继续处理其他图像，不中断整个过程
            
            # 重新显示当前图像
            self.show_current_image()
            
            # 记录历史
            history_record = self.history_logger.add_record(
                "批量处理", 
                f"已对所有 {len(self.images)} 张图像进行垂直翻转"
            )
            self.update_history_display(history_record)
            
            # 显示状态栏消息
            self.statusBar().showMessage(f"已对所有 {len(self.images)} 张图像进行垂直翻转")
            
        except Exception as e:
            print(f"垂直翻转图像时出错: {e}")
            import traceback
            traceback.print_exc()  # 打印详细错误信息
            self.statusBar().showMessage(f"垂直翻转失败: {e}")
            
    def rotate_left(self):
        """向左旋转当前图像90度（逆时针）"""
        if not self.images or self.current_index >= len(self.images):
            return
            
        try:
            # 获取当前图像
            current_image = self.images[self.current_index]
            current_pixmap = current_image['pixmap']
            
            # 创建向左旋转90度的QPixmap
            rotated_pixmap = current_pixmap.transformed(QTransform().rotate(-90))
            
            # 更新图像
            current_image['pixmap'] = rotated_pixmap
            
            # 同时更新original_images中的数据（如果存在）
            if self.current_index < len(self.original_images):
                try:
                    # 获取原始图像数据
                    original_data = self.original_images[self.current_index]
                    if original_data is not None and isinstance(original_data, np.ndarray):
                        # 逆时针旋转numpy数组90度
                        rotated_data = np.rot90(original_data)
                        # 更新原始图像数据
                        self.original_images[self.current_index] = rotated_data
                except Exception as e:
                    print(f"旋转original_images[{self.current_index}]时出错: {e}")
            
            # 重新显示当前图像
            self.show_current_image()
            
            # 记录历史
            history_record = self.history_logger.add_record(
                "旋转", 
                "已向左旋转当前图像90度"
            )
            self.update_history_display(history_record)
            
            # 显示状态栏消息
            self.statusBar().showMessage("已向左旋转当前图像90度")
            
        except Exception as e:
            print(f"向左旋转图像时出错: {e}")
            import traceback
            traceback.print_exc()
            self.statusBar().showMessage(f"向左旋转失败: {e}")
            
    def rotate_right(self):
        """向右旋转当前图像90度（顺时针）"""
        if not self.images or self.current_index >= len(self.images):
            return
            
        try:
            # 获取当前图像
            current_image = self.images[self.current_index]
            current_pixmap = current_image['pixmap']
            
            # 创建向右旋转90度的QPixmap
            rotated_pixmap = current_pixmap.transformed(QTransform().rotate(90))
            
            # 更新图像
            current_image['pixmap'] = rotated_pixmap
            
            # 同时更新original_images中的数据（如果存在）
            if self.current_index < len(self.original_images):
                try:
                    # 获取原始图像数据
                    original_data = self.original_images[self.current_index]
                    if original_data is not None and isinstance(original_data, np.ndarray):
                        # 顺时针旋转numpy数组90度
                        rotated_data = np.rot90(original_data, k=3)
                        # 更新原始图像数据
                        self.original_images[self.current_index] = rotated_data
                except Exception as e:
                    print(f"旋转original_images[{self.current_index}]时出错: {e}")
            
            # 重新显示当前图像
            self.show_current_image()
            
            # 记录历史
            history_record = self.history_logger.add_record(
                "旋转", 
                "已向右旋转当前图像90度"
            )
            self.update_history_display(history_record)
            
            # 显示状态栏消息
            self.statusBar().showMessage("已向右旋转当前图像90度")
            
        except Exception as e:
            print(f"向右旋转图像时出错: {e}")
            import traceback
            traceback.print_exc()
            self.statusBar().showMessage(f"向右旋转失败: {e}")
    


class FitsHeaderDialog(QDialog):
    """FITS头文件信息对话框"""
    def __init__(self, parent=None, header_info=None):
        super().__init__(parent)
        self.setWindowTitle("FITS头文件信息")
        self.resize(600, 500)
        
        # 创建布局
        layout = QVBoxLayout(self)
        
        # 创建文本编辑器用于显示头信息
        self.header_text = QTextEdit()
        self.header_text.setReadOnly(True)
        self.header_text.setFont(QFont("Courier New", 10))
        layout.addWidget(self.header_text)
        
        # 创建搜索框
        search_layout = QHBoxLayout()
        search_label = QLabel("搜索:")
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("输入关键字搜索...")
        self.search_edit.textChanged.connect(self.search_text)
        search_layout.addWidget(search_label)
        search_layout.addWidget(self.search_edit)
        layout.addLayout(search_layout)
        
        # 创建按钮
        button_box = QDialogButtonBox(QDialogButtonBox.Ok)
        button_box.accepted.connect(self.accept)
        layout.addWidget(button_box)
        
        # 设置头信息
        if header_info:
            self.set_header_info(header_info)
    
    def set_header_info(self, header_info):
        """设置头信息"""
        if not header_info:
            self.header_text.setText("无FITS头文件信息")
            return
            
        # 格式化头信息
        header_text = ""
        for key, value in header_info.items():
            header_text += f"{key:8} = {value}\n"
        
        self.header_text.setText(header_text)
    
    def search_text(self):
        """搜索文本"""
        search_text = self.search_edit.text()
        if not search_text:
            # 清除所有高亮
            cursor = self.header_text.textCursor()
            cursor.setPosition(0)
            self.header_text.setTextCursor(cursor)
            return
        
        # 获取当前光标位置
        cursor = self.header_text.textCursor()
        current_pos = cursor.position()
        
        # 从当前位置开始搜索
        cursor.setPosition(0)
        self.header_text.setTextCursor(cursor)
        
        # 查找文本
        found = self.header_text.find(search_text)
        if not found:
            # 如果没找到，从头开始搜索
            cursor.setPosition(0)
            self.header_text.setTextCursor(cursor)
            self.header_text.find(search_text)

def launch_image_viewer():
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.showMaximized()
    return app.exec_()

# 只有在直接运行此文件时才启动应用
if __name__ == "__main__":
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.showMaximized()
    sys.exit(app.exec_())
